Explorar o código

Rewrite heatmap to class

Tobias Skarhed %!s(int64=7) %!d(string=hai) anos
pai
achega
076bfea362

+ 1 - 1
public/app/plugins/panel/heatmap/heatmap_ctrl.ts

@@ -358,6 +358,6 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
   }
   }
 
 
   link(scope, elem, attrs, ctrl) {
   link(scope, elem, attrs, ctrl) {
-    rendering(scope, elem, attrs, ctrl);
+    let render = new rendering(scope, elem, attrs, ctrl);
   }
   }
 }
 }

+ 358 - 350
public/app/plugins/panel/heatmap/rendering.ts

@@ -19,56 +19,91 @@ let MIN_CARD_SIZE = 1,
   Y_AXIS_TICK_PADDING = 5,
   Y_AXIS_TICK_PADDING = 5,
   MIN_SELECTION_WIDTH = 2;
   MIN_SELECTION_WIDTH = 2;
 
 
-export default function link(scope, elem, attrs, ctrl) {
-  let data, timeRange, panel, heatmap;
-
-  // $heatmap is JQuery object, but heatmap is D3
-  let $heatmap = elem.find('.heatmap-panel');
-  let tooltip = new HeatmapTooltip($heatmap, scope);
-
-  let width,
-    height,
-    yScale,
-    xScale,
-    chartWidth,
-    chartHeight,
-    chartTop,
-    chartBottom,
-    yAxisWidth,
-    xAxisHeight,
-    cardPadding,
-    cardRound,
-    cardWidth,
-    cardHeight,
-    colorScale,
-    opacityScale,
-    mouseUpHandler;
-
-  let selection = {
-    active: false,
-    x1: -1,
-    x2: -1,
-  };
-
-  let padding = { left: 0, right: 0, top: 0, bottom: 0 },
-    margin = { left: 25, right: 15, top: 10, bottom: 20 },
-    dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;
-
-  ctrl.events.on('render', () => {
-    render();
-    ctrl.renderingCompleted();
-  });
-
-  function setElementHeight() {
+export default class Link {
+  width: number;
+  height: number;
+  yScale: any;
+  xScale: any;
+  chartWidth: number;
+  chartHeight: number;
+  chartTop: number;
+  chartBottom: number;
+  yAxisWidth: number;
+  xAxisHeight: number;
+  cardPadding: number;
+  cardRound: number;
+  cardWidth: number;
+  cardHeight: number;
+  colorScale: any;
+  opacityScale: any;
+  mouseUpHandler: any;
+  data: any;
+  panel: any;
+  $heatmap: any;
+  tooltip: HeatmapTooltip;
+  heatmap: any;
+  timeRange: any;
+
+  selection: any;
+  padding: any;
+  margin: any;
+  dataRangeWidingFactor: number;
+  constructor(private scope, private elem, attrs, private ctrl) {
+    // $heatmap is JQuery object, but heatmap is D3
+    this.$heatmap = elem.find('.heatmap-panel');
+    this.tooltip = new HeatmapTooltip(this.$heatmap, this.scope);
+
+    this.selection = {
+      active: false,
+      x1: -1,
+      x2: -1,
+    };
+
+    this.padding = { left: 0, right: 0, top: 0, bottom: 0 };
+    this.margin = { left: 25, right: 15, top: 10, bottom: 20 };
+    this.dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;
+
+    this.ctrl.events.on('render', this.onRender.bind(this));
+
+    this.ctrl.tickValueFormatter = this.tickValueFormatter;
+    /////////////////////////////
+    // Selection and crosshair //
+    /////////////////////////////
+
+    // Shared crosshair and tooltip
+    appEvents.on('graph-hover', this.onGraphHover.bind(this), this.scope);
+
+    appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), this.scope);
+
+    // Register selection listeners
+    this.$heatmap.on('mousedown', this.onMouseDown.bind(this));
+    this.$heatmap.on('mousemove', this.onMouseMove.bind(this));
+    this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this));
+  }
+
+  onGraphHoverClear() {
+    this.clearCrosshair();
+  }
+
+  onGraphHover(event) {
+    this.drawSharedCrosshair(event.pos);
+  }
+
+  onRender() {
+    this.render();
+    this.ctrl.renderingCompleted();
+  }
+
+  setElementHeight() {
     try {
     try {
-      var height = ctrl.height || panel.height || ctrl.row.height;
+      var height = this.ctrl.height || this.panel.height || this.ctrl.row.height;
       if (_.isString(height)) {
       if (_.isString(height)) {
         height = parseInt(height.replace('px', ''), 10);
         height = parseInt(height.replace('px', ''), 10);
       }
       }
 
 
-      height -= panel.legend.show ? 28 : 11; // bottom padding and space for legend
+      height -= this.panel.legend.show ? 28 : 11; // bottom padding and space for legend
 
 
-      $heatmap.css('height', height + 'px');
+      this.$heatmap.css('height', height + 'px');
 
 
       return true;
       return true;
     } catch (e) {
     } catch (e) {
@@ -77,7 +112,7 @@ export default function link(scope, elem, attrs, ctrl) {
     }
     }
   }
   }
 
 
-  function getYAxisWidth(elem) {
+  getYAxisWidth(elem) {
     let axis_text = elem.selectAll('.axis-y text').nodes();
     let axis_text = elem.selectAll('.axis-y text').nodes();
     let max_text_width = _.max(
     let max_text_width = _.max(
       _.map(axis_text, text => {
       _.map(axis_text, text => {
@@ -89,7 +124,7 @@ export default function link(scope, elem, attrs, ctrl) {
     return max_text_width;
     return max_text_width;
   }
   }
 
 
-  function getXAxisHeight(elem) {
+  getXAxisHeight(elem) {
     let axis_line = elem.select('.axis-x line');
     let axis_line = elem.select('.axis-x line');
     if (!axis_line.empty()) {
     if (!axis_line.empty()) {
       let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2'));
       let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2'));
@@ -101,16 +136,16 @@ export default function link(scope, elem, attrs, ctrl) {
     }
     }
   }
   }
 
 
-  function addXAxis() {
-    scope.xScale = xScale = d3
+  addXAxis() {
+    this.scope.xScale = this.xScale = d3
       .scaleTime()
       .scaleTime()
-      .domain([timeRange.from, timeRange.to])
-      .range([0, chartWidth]);
+      .domain([this.timeRange.from, this.timeRange.to])
+      .range([0, this.chartWidth]);
 
 
-    let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX;
-    let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, timeRange.from, timeRange.to);
+    let ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
+    let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
     let timeFormat;
     let timeFormat;
-    let dashboardTimeZone = ctrl.dashboard.getTimezone();
+    let dashboardTimeZone = this.ctrl.dashboard.getTimezone();
     if (dashboardTimeZone === 'utc') {
     if (dashboardTimeZone === 'utc') {
       timeFormat = d3.utcFormat(grafanaTimeFormatter);
       timeFormat = d3.utcFormat(grafanaTimeFormatter);
     } else {
     } else {
@@ -118,100 +153,100 @@ export default function link(scope, elem, attrs, ctrl) {
     }
     }
 
 
     let xAxis = d3
     let xAxis = d3
-      .axisBottom(xScale)
+      .axisBottom(this.xScale)
       .ticks(ticks)
       .ticks(ticks)
       .tickFormat(timeFormat)
       .tickFormat(timeFormat)
       .tickPadding(X_AXIS_TICK_PADDING)
       .tickPadding(X_AXIS_TICK_PADDING)
-      .tickSize(chartHeight);
+      .tickSize(this.chartHeight);
 
 
-    let posY = margin.top;
-    let posX = yAxisWidth;
-    heatmap
+    let posY = this.margin.top;
+    let posX = this.yAxisWidth;
+    this.heatmap
       .append('g')
       .append('g')
       .attr('class', 'axis axis-x')
       .attr('class', 'axis axis-x')
       .attr('transform', 'translate(' + posX + ',' + posY + ')')
       .attr('transform', 'translate(' + posX + ',' + posY + ')')
       .call(xAxis);
       .call(xAxis);
 
 
     // Remove horizontal line in the top of axis labels (called domain in d3)
     // Remove horizontal line in the top of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-x')
       .select('.axis-x')
       .select('.domain')
       .select('.domain')
       .remove();
       .remove();
   }
   }
 
 
-  function addYAxis() {
-    let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX);
-    let tick_interval = ticksUtils.tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks);
-    let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval);
+  addYAxis() {
+    let ticks = Math.ceil(this.chartHeight / DEFAULT_Y_TICK_SIZE_PX);
+    let tick_interval = ticksUtils.tickStep(this.data.heatmapStats.min, this.data.heatmapStats.max, ticks);
+    let { y_min, y_max } = this.wideYAxisRange(this.data.heatmapStats.min, this.data.heatmapStats.max, tick_interval);
 
 
     // Rewrite min and max if it have been set explicitly
     // Rewrite min and max if it have been set explicitly
-    y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min;
-    y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max;
+    y_min = this.panel.yAxis.min !== null ? this.panel.yAxis.min : y_min;
+    y_max = this.panel.yAxis.max !== null ? this.panel.yAxis.max : y_max;
 
 
     // Adjust ticks after Y range widening
     // Adjust ticks after Y range widening
     tick_interval = ticksUtils.tickStep(y_min, y_max, ticks);
     tick_interval = ticksUtils.tickStep(y_min, y_max, ticks);
     ticks = Math.ceil((y_max - y_min) / tick_interval);
     ticks = Math.ceil((y_max - y_min) / tick_interval);
 
 
     let decimalsAuto = ticksUtils.getPrecision(tick_interval);
     let decimalsAuto = ticksUtils.getPrecision(tick_interval);
-    let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
+    let decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
     // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
     // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
     let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
     let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
     let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
     let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
-    ctrl.decimals = decimals;
-    ctrl.scaledDecimals = scaledDecimals;
+    this.ctrl.decimals = decimals;
+    this.ctrl.scaledDecimals = scaledDecimals;
 
 
     // Set default Y min and max if no data
     // Set default Y min and max if no data
-    if (_.isEmpty(data.buckets)) {
+    if (_.isEmpty(this.data.buckets)) {
       y_max = 1;
       y_max = 1;
       y_min = -1;
       y_min = -1;
       ticks = 3;
       ticks = 3;
       decimals = 1;
       decimals = 1;
     }
     }
 
 
-    data.yAxis = {
+    this.data.yAxis = {
       min: y_min,
       min: y_min,
       max: y_max,
       max: y_max,
       ticks: ticks,
       ticks: ticks,
     };
     };
 
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLinear()
       .scaleLinear()
       .domain([y_min, y_max])
       .domain([y_min, y_max])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
 
     let yAxis = d3
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .ticks(ticks)
       .ticks(ticks)
-      .tickFormat(tickValueFormatter(decimals, scaledDecimals))
-      .tickSizeInner(0 - width)
+      .tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
       .tickPadding(Y_AXIS_TICK_PADDING);
 
 
-    heatmap
+    this.heatmap
       .append('g')
       .append('g')
       .attr('class', 'axis axis-y')
       .attr('class', 'axis axis-y')
       .call(yAxis);
       .call(yAxis);
 
 
     // Calculate Y axis width first, then move axis into visible area
     // Calculate Y axis width first, then move axis into visible area
-    let posY = margin.top;
-    let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    let posY = this.margin.top;
+    let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
 
     // Remove vertical line in the right of axis labels (called domain in d3)
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.axis-y')
       .select('.domain')
       .select('.domain')
       .remove();
       .remove();
   }
   }
 
 
   // Wide Y values range and anjust to bucket size
   // Wide Y values range and anjust to bucket size
-  function wideYAxisRange(min, max, tickInterval) {
-    let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
+  wideYAxisRange(min, max, tickInterval) {
+    let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2;
     let y_min, y_max;
     let y_min, y_max;
 
 
     if (tickInterval === 0) {
     if (tickInterval === 0) {
-      y_max = max * dataRangeWidingFactor;
-      y_min = min - min * (dataRangeWidingFactor - 1);
+      y_max = max * this.dataRangeWidingFactor;
+      y_min = min - min * (this.dataRangeWidingFactor - 1);
       tickInterval = (y_max - y_min) / 2;
       tickInterval = (y_max - y_min) / 2;
     } else {
     } else {
       y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval;
       y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval;
@@ -226,152 +261,153 @@ export default function link(scope, elem, attrs, ctrl) {
     return { y_min, y_max };
     return { y_min, y_max };
   }
   }
 
 
-  function addLogYAxis() {
-    let log_base = panel.yAxis.logBase;
-    let { y_min, y_max } = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base);
+  addLogYAxis() {
+    let log_base = this.panel.yAxis.logBase;
+    let { y_min, y_max } = this.adjustLogRange(this.data.heatmapStats.minLog, this.data.heatmapStats.max, log_base);
 
 
-    y_min = panel.yAxis.min && panel.yAxis.min !== '0' ? adjustLogMin(panel.yAxis.min, log_base) : y_min;
-    y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max;
+    y_min =
+      this.panel.yAxis.min && this.panel.yAxis.min !== '0' ? this.adjustLogMin(this.panel.yAxis.min, log_base) : y_min;
+    y_max = this.panel.yAxis.max !== null ? this.adjustLogMax(this.panel.yAxis.max, log_base) : y_max;
 
 
     // Set default Y min and max if no data
     // Set default Y min and max if no data
-    if (_.isEmpty(data.buckets)) {
+    if (_.isEmpty(this.data.buckets)) {
       y_max = Math.pow(log_base, 2);
       y_max = Math.pow(log_base, 2);
       y_min = 1;
       y_min = 1;
     }
     }
 
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLog()
       .scaleLog()
-      .base(panel.yAxis.logBase)
+      .base(this.panel.yAxis.logBase)
       .domain([y_min, y_max])
       .domain([y_min, y_max])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
 
-    let domain = yScale.domain();
-    let tick_values = logScaleTickValues(domain, log_base);
+    let domain = this.yScale.domain();
+    let tick_values = this.logScaleTickValues(domain, log_base);
 
 
     let decimalsAuto = ticksUtils.getPrecision(y_min);
     let decimalsAuto = ticksUtils.getPrecision(y_min);
-    let decimals = panel.yAxis.decimals || decimalsAuto;
+    let decimals = this.panel.yAxis.decimals || decimalsAuto;
 
 
     // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
     // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
     let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto);
     let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto);
     let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
     let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
-    ctrl.decimals = decimals;
-    ctrl.scaledDecimals = scaledDecimals;
+    this.ctrl.decimals = decimals;
+    this.ctrl.scaledDecimals = scaledDecimals;
 
 
-    data.yAxis = {
+    this.data.yAxis = {
       min: y_min,
       min: y_min,
       max: y_max,
       max: y_max,
       ticks: tick_values.length,
       ticks: tick_values.length,
     };
     };
 
 
     let yAxis = d3
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .tickValues(tick_values)
       .tickValues(tick_values)
-      .tickFormat(tickValueFormatter(decimals, scaledDecimals))
-      .tickSizeInner(0 - width)
+      .tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
       .tickPadding(Y_AXIS_TICK_PADDING);
 
 
-    heatmap
+    this.heatmap
       .append('g')
       .append('g')
       .attr('class', 'axis axis-y')
       .attr('class', 'axis axis-y')
       .call(yAxis);
       .call(yAxis);
 
 
     // Calculate Y axis width first, then move axis into visible area
     // Calculate Y axis width first, then move axis into visible area
-    let posY = margin.top;
-    let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    let posY = this.margin.top;
+    let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
 
     // Set first tick as pseudo 0
     // Set first tick as pseudo 0
     if (y_min < 1) {
     if (y_min < 1) {
-      heatmap
+      this.heatmap
         .select('.axis-y')
         .select('.axis-y')
         .select('.tick text')
         .select('.tick text')
         .text('0');
         .text('0');
     }
     }
 
 
     // Remove vertical line in the right of axis labels (called domain in d3)
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.axis-y')
       .select('.domain')
       .select('.domain')
       .remove();
       .remove();
   }
   }
 
 
-  function addYAxisFromBuckets() {
-    const tsBuckets = data.tsBuckets;
+  addYAxisFromBuckets() {
+    const tsBuckets = this.data.tsBuckets;
 
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLinear()
       .scaleLinear()
       .domain([0, tsBuckets.length - 1])
       .domain([0, tsBuckets.length - 1])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
 
     const tick_values = _.map(tsBuckets, (b, i) => i);
     const tick_values = _.map(tsBuckets, (b, i) => i);
     const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision));
     const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision));
-    const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
-    ctrl.decimals = decimals;
+    const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
+    this.ctrl.decimals = decimals;
 
 
     function tickFormatter(valIndex) {
     function tickFormatter(valIndex) {
       let valueFormatted = tsBuckets[valIndex];
       let valueFormatted = tsBuckets[valIndex];
       if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
       if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
         // Try to format numeric tick labels
         // Try to format numeric tick labels
-        valueFormatted = tickValueFormatter(decimals)(_.toNumber(valueFormatted));
+        valueFormatted = this.tickValueFormatter(decimals)(_.toNumber(valueFormatted));
       }
       }
       return valueFormatted;
       return valueFormatted;
     }
     }
 
 
     const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i));
     const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i));
-    data.tsBucketsFormatted = tsBucketsFormatted;
+    this.data.tsBucketsFormatted = tsBucketsFormatted;
 
 
     let yAxis = d3
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .tickValues(tick_values)
       .tickValues(tick_values)
       .tickFormat(tickFormatter)
       .tickFormat(tickFormatter)
-      .tickSizeInner(0 - width)
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
       .tickPadding(Y_AXIS_TICK_PADDING);
 
 
-    heatmap
+    this.heatmap
       .append('g')
       .append('g')
       .attr('class', 'axis axis-y')
       .attr('class', 'axis axis-y')
       .call(yAxis);
       .call(yAxis);
 
 
     // Calculate Y axis width first, then move axis into visible area
     // Calculate Y axis width first, then move axis into visible area
-    const posY = margin.top;
-    const posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    const posY = this.margin.top;
+    const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
 
     // Remove vertical line in the right of axis labels (called domain in d3)
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.axis-y')
       .select('.domain')
       .select('.domain')
       .remove();
       .remove();
   }
   }
 
 
   // Adjust data range to log base
   // Adjust data range to log base
-  function adjustLogRange(min, max, logBase) {
+  adjustLogRange(min, max, logBase) {
     let y_min, y_max;
     let y_min, y_max;
 
 
-    y_min = data.heatmapStats.minLog;
-    if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) {
+    y_min = this.data.heatmapStats.minLog;
+    if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) {
       y_min = 1;
       y_min = 1;
     } else {
     } else {
-      y_min = adjustLogMin(data.heatmapStats.minLog, logBase);
+      y_min = this.adjustLogMin(this.data.heatmapStats.minLog, logBase);
     }
     }
 
 
     // Adjust max Y value to log base
     // Adjust max Y value to log base
-    y_max = adjustLogMax(data.heatmapStats.max, logBase);
+    y_max = this.adjustLogMax(this.data.heatmapStats.max, logBase);
 
 
     return { y_min, y_max };
     return { y_min, y_max };
   }
   }
 
 
-  function adjustLogMax(max, base) {
+  adjustLogMax(max, base) {
     return Math.pow(base, Math.ceil(ticksUtils.logp(max, base)));
     return Math.pow(base, Math.ceil(ticksUtils.logp(max, base)));
   }
   }
 
 
-  function adjustLogMin(min, base) {
+  adjustLogMin(min, base) {
     return Math.pow(base, Math.floor(ticksUtils.logp(min, base)));
     return Math.pow(base, Math.floor(ticksUtils.logp(min, base)));
   }
   }
 
 
-  function logScaleTickValues(domain, base) {
+  logScaleTickValues(domain, base) {
     let domainMin = domain[0];
     let domainMin = domain[0];
     let domainMax = domain[1];
     let domainMax = domain[1];
     let tickValues = [];
     let tickValues = [];
@@ -393,8 +429,8 @@ export default function link(scope, elem, attrs, ctrl) {
     return tickValues;
     return tickValues;
   }
   }
 
 
-  function tickValueFormatter(decimals, scaledDecimals = null) {
-    let format = panel.yAxis.format;
+  tickValueFormatter(decimals, scaledDecimals = null) {
+    let format = this.panel.yAxis.format;
     return function(value) {
     return function(value) {
       try {
       try {
         return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value;
         return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value;
@@ -405,181 +441,179 @@ export default function link(scope, elem, attrs, ctrl) {
     };
     };
   }
   }
 
 
-  ctrl.tickValueFormatter = tickValueFormatter;
-
-  function fixYAxisTickSize() {
-    heatmap
+  fixYAxisTickSize() {
+    this.heatmap
       .select('.axis-y')
       .select('.axis-y')
       .selectAll('.tick line')
       .selectAll('.tick line')
-      .attr('x2', chartWidth);
+      .attr('x2', this.chartWidth);
   }
   }
 
 
-  function addAxes() {
-    chartHeight = height - margin.top - margin.bottom;
-    chartTop = margin.top;
-    chartBottom = chartTop + chartHeight;
+  addAxes() {
+    this.chartHeight = this.height - this.margin.top - this.margin.bottom;
+    this.chartTop = this.margin.top;
+    this.chartBottom = this.chartTop + this.chartHeight;
 
 
-    if (panel.dataFormat === 'tsbuckets') {
-      addYAxisFromBuckets();
+    if (this.panel.dataFormat === 'tsbuckets') {
+      this.addYAxisFromBuckets();
     } else {
     } else {
-      if (panel.yAxis.logBase === 1) {
-        addYAxis();
+      if (this.panel.yAxis.logBase === 1) {
+        this.addYAxis();
       } else {
       } else {
-        addLogYAxis();
+        this.addLogYAxis();
       }
       }
     }
     }
 
 
-    yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    chartWidth = width - yAxisWidth - margin.right;
-    fixYAxisTickSize();
+    this.yAxisWidth = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.chartWidth = this.width - this.yAxisWidth - this.margin.right;
+    this.fixYAxisTickSize();
 
 
-    addXAxis();
-    xAxisHeight = getXAxisHeight(heatmap);
+    this.addXAxis();
+    this.xAxisHeight = this.getXAxisHeight(this.heatmap);
 
 
-    if (!panel.yAxis.show) {
-      heatmap
+    if (!this.panel.yAxis.show) {
+      this.heatmap
         .select('.axis-y')
         .select('.axis-y')
         .selectAll('line')
         .selectAll('line')
         .style('opacity', 0);
         .style('opacity', 0);
     }
     }
 
 
-    if (!panel.xAxis.show) {
-      heatmap
+    if (!this.panel.xAxis.show) {
+      this.heatmap
         .select('.axis-x')
         .select('.axis-x')
         .selectAll('line')
         .selectAll('line')
         .style('opacity', 0);
         .style('opacity', 0);
     }
     }
   }
   }
 
 
-  function addHeatmapCanvas() {
-    let heatmap_elem = $heatmap[0];
+  addHeatmapCanvas() {
+    let heatmap_elem = this.$heatmap[0];
 
 
-    width = Math.floor($heatmap.width()) - padding.right;
-    height = Math.floor($heatmap.height()) - padding.bottom;
+    this.width = Math.floor(this.$heatmap.width()) - this.padding.right;
+    this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom;
 
 
-    cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
-    cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
+    this.cardPadding = this.panel.cards.cardPadding !== null ? this.panel.cards.cardPadding : CARD_PADDING;
+    this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND;
 
 
-    if (heatmap) {
-      heatmap.remove();
+    if (this.heatmap) {
+      this.heatmap.remove();
     }
     }
 
 
-    heatmap = d3
+    this.heatmap = d3
       .select(heatmap_elem)
       .select(heatmap_elem)
       .append('svg')
       .append('svg')
-      .attr('width', width)
-      .attr('height', height);
+      .attr('width', this.width)
+      .attr('height', this.height);
   }
   }
 
 
-  function addHeatmap() {
-    addHeatmapCanvas();
-    addAxes();
+  addHeatmap() {
+    this.addHeatmapCanvas();
+    this.addAxes();
 
 
-    if (panel.yAxis.logBase !== 1 && panel.dataFormat !== 'tsbuckets') {
-      let log_base = panel.yAxis.logBase;
-      let domain = yScale.domain();
-      let tick_values = logScaleTickValues(domain, log_base);
-      data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
+    if (this.panel.yAxis.logBase !== 1 && this.panel.dataFormat !== 'tsbuckets') {
+      let log_base = this.panel.yAxis.logBase;
+      let domain = this.yScale.domain();
+      let tick_values = this.logScaleTickValues(domain, log_base);
+      this.data.buckets = mergeZeroBuckets(this.data.buckets, _.min(tick_values));
     }
     }
 
 
-    let cardsData = data.cards;
-    let maxValueAuto = data.cardStats.max;
-    let maxValue = panel.color.max || maxValueAuto;
-    let minValue = panel.color.min || 0;
+    let cardsData = this.data.cards;
+    let maxValueAuto = this.data.cardStats.max;
+    let maxValue = this.panel.color.max || maxValueAuto;
+    let minValue = this.panel.color.min || 0;
 
 
-    let colorScheme = _.find(ctrl.colorSchemes, {
-      value: panel.color.colorScheme,
+    let colorScheme = _.find(this.ctrl.colorSchemes, {
+      value: this.panel.color.colorScheme,
     });
     });
-    colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
-    opacityScale = getOpacityScale(panel.color, maxValue);
-    setCardSize();
+    this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
+    this.opacityScale = getOpacityScale(this.panel.color, maxValue);
+    this.setCardSize();
 
 
-    let cards = heatmap.selectAll('.heatmap-card').data(cardsData);
+    let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData);
     cards.append('title');
     cards.append('title');
     cards = cards
     cards = cards
       .enter()
       .enter()
       .append('rect')
       .append('rect')
-      .attr('x', getCardX)
-      .attr('width', getCardWidth)
-      .attr('y', getCardY)
-      .attr('height', getCardHeight)
-      .attr('rx', cardRound)
-      .attr('ry', cardRound)
+      .attr('x', this.getCardX)
+      .attr('width', this.getCardWidth)
+      .attr('y', this.getCardY)
+      .attr('height', this.getCardHeight)
+      .attr('rx', this.cardRound)
+      .attr('ry', this.cardRound)
       .attr('class', 'bordered heatmap-card')
       .attr('class', 'bordered heatmap-card')
-      .style('fill', getCardColor)
-      .style('stroke', getCardColor)
+      .style('fill', this.getCardColor)
+      .style('stroke', this.getCardColor)
       .style('stroke-width', 0)
       .style('stroke-width', 0)
-      .style('opacity', getCardOpacity);
+      .style('opacity', this.getCardOpacity);
 
 
-    let $cards = $heatmap.find('.heatmap-card');
+    let $cards = this.$heatmap.find('.heatmap-card');
     $cards
     $cards
       .on('mouseenter', event => {
       .on('mouseenter', event => {
-        tooltip.mouseOverBucket = true;
-        highlightCard(event);
+        this.tooltip.mouseOverBucket = true;
+        this.highlightCard(event);
       })
       })
       .on('mouseleave', event => {
       .on('mouseleave', event => {
-        tooltip.mouseOverBucket = false;
-        resetCardHighLight(event);
+        this.tooltip.mouseOverBucket = false;
+        this.resetCardHighLight(event);
       });
       });
   }
   }
 
 
-  function highlightCard(event) {
+  highlightCard(event) {
     let color = d3.select(event.target).style('fill');
     let color = d3.select(event.target).style('fill');
     let highlightColor = d3.color(color).darker(2);
     let highlightColor = d3.color(color).darker(2);
     let strokeColor = d3.color(color).brighter(4);
     let strokeColor = d3.color(color).brighter(4);
     let current_card = d3.select(event.target);
     let current_card = d3.select(event.target);
-    tooltip.originalFillColor = color;
+    this.tooltip.originalFillColor = color;
     current_card
     current_card
       .style('fill', highlightColor.toString())
       .style('fill', highlightColor.toString())
       .style('stroke', strokeColor.toString())
       .style('stroke', strokeColor.toString())
       .style('stroke-width', 1);
       .style('stroke-width', 1);
   }
   }
 
 
-  function resetCardHighLight(event) {
+  resetCardHighLight(event) {
     d3
     d3
       .select(event.target)
       .select(event.target)
-      .style('fill', tooltip.originalFillColor)
-      .style('stroke', tooltip.originalFillColor)
+      .style('fill', this.tooltip.originalFillColor)
+      .style('stroke', this.tooltip.originalFillColor)
       .style('stroke-width', 0);
       .style('stroke-width', 0);
   }
   }
 
 
-  function setCardSize() {
-    let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0));
-    let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize));
+  setCardSize() {
+    let xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0));
+    let yGridSize = Math.floor(this.yScale(this.yScale.invert(0) - this.data.yBucketSize));
 
 
-    if (panel.yAxis.logBase !== 1) {
-      let base = panel.yAxis.logBase;
-      let splitFactor = data.yBucketSize || 1;
-      yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor);
+    if (this.panel.yAxis.logBase !== 1) {
+      let base = this.panel.yAxis.logBase;
+      let splitFactor = this.data.yBucketSize || 1;
+      yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor);
     }
     }
 
 
-    cardWidth = xGridSize - cardPadding * 2;
-    cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
+    this.cardWidth = xGridSize - this.cardPadding * 2;
+    this.cardHeight = yGridSize ? yGridSize - this.cardPadding * 2 : 0;
   }
   }
 
 
-  function getCardX(d) {
+  getCardX(d) {
     let x;
     let x;
-    if (xScale(d.x) < 0) {
+    if (this.xScale(d.x) < 0) {
       // Cut card left to prevent overlay
       // Cut card left to prevent overlay
-      x = yAxisWidth + cardPadding;
+      x = this.yAxisWidth + this.cardPadding;
     } else {
     } else {
-      x = xScale(d.x) + yAxisWidth + cardPadding;
+      x = this.xScale(d.x) + this.yAxisWidth + this.cardPadding;
     }
     }
 
 
     return x;
     return x;
   }
   }
 
 
-  function getCardWidth(d) {
+  getCardWidth(d) {
     let w;
     let w;
-    if (xScale(d.x) < 0) {
+    if (this.xScale(d.x) < 0) {
       // Cut card left to prevent overlay
       // Cut card left to prevent overlay
-      let cutted_width = xScale(d.x) + cardWidth;
+      let cutted_width = this.xScale(d.x) + this.cardWidth;
       w = cutted_width > 0 ? cutted_width : 0;
       w = cutted_width > 0 ? cutted_width : 0;
-    } else if (xScale(d.x) + cardWidth > chartWidth) {
+    } else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) {
       // Cut card right to prevent overlay
       // Cut card right to prevent overlay
-      w = chartWidth - xScale(d.x) - cardPadding;
+      w = this.chartWidth - this.xScale(d.x) - this.cardPadding;
     } else {
     } else {
-      w = cardWidth;
+      w = this.cardWidth;
     }
     }
 
 
     // Card width should be MIN_CARD_SIZE at least
     // Card width should be MIN_CARD_SIZE at least
@@ -587,138 +621,117 @@ export default function link(scope, elem, attrs, ctrl) {
     return w;
     return w;
   }
   }
 
 
-  function getCardY(d) {
-    let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
-    if (panel.yAxis.logBase !== 1 && d.y === 0) {
-      y = chartBottom - cardHeight - cardPadding;
+  getCardY(d) {
+    let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding;
+    if (this.panel.yAxis.logBase !== 1 && d.y === 0) {
+      y = this.chartBottom - this.cardHeight - this.cardPadding;
     } else {
     } else {
-      if (y < chartTop) {
-        y = chartTop;
+      if (y < this.chartTop) {
+        y = this.chartTop;
       }
       }
     }
     }
 
 
     return y;
     return y;
   }
   }
 
 
-  function getCardHeight(d) {
-    let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
-    let h = cardHeight;
+  getCardHeight(d) {
+    let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding;
+    let h = this.cardHeight;
 
 
-    if (panel.yAxis.logBase !== 1 && d.y === 0) {
-      return cardHeight;
+    if (this.panel.yAxis.logBase !== 1 && d.y === 0) {
+      return this.cardHeight;
     }
     }
 
 
     // Cut card height to prevent overlay
     // Cut card height to prevent overlay
-    if (y < chartTop) {
-      h = yScale(d.y) - cardPadding;
-    } else if (yScale(d.y) > chartBottom) {
-      h = chartBottom - y;
-    } else if (y + cardHeight > chartBottom) {
-      h = chartBottom - y;
+    if (y < this.chartTop) {
+      h = this.yScale(d.y) - this.cardPadding;
+    } else if (this.yScale(d.y) > this.chartBottom) {
+      h = this.chartBottom - y;
+    } else if (y + this.cardHeight > this.chartBottom) {
+      h = this.chartBottom - y;
     }
     }
 
 
     // Height can't be more than chart height
     // Height can't be more than chart height
-    h = Math.min(h, chartHeight);
+    h = Math.min(h, this.chartHeight);
     // Card height should be MIN_CARD_SIZE at least
     // Card height should be MIN_CARD_SIZE at least
     h = Math.max(h, MIN_CARD_SIZE);
     h = Math.max(h, MIN_CARD_SIZE);
 
 
     return h;
     return h;
   }
   }
 
 
-  function getCardColor(d) {
-    if (panel.color.mode === 'opacity') {
-      return panel.color.cardColor;
+  getCardColor(d) {
+    if (this.panel.color.mode === 'opacity') {
+      return this.panel.color.cardColor;
     } else {
     } else {
-      return colorScale(d.count);
+      return this.colorScale(d.count);
     }
     }
   }
   }
 
 
-  function getCardOpacity(d) {
-    if (panel.color.mode === 'opacity') {
-      return opacityScale(d.count);
+  getCardOpacity(d) {
+    if (this.panel.color.mode === 'opacity') {
+      return this.opacityScale(d.count);
     } else {
     } else {
       return 1;
       return 1;
     }
     }
   }
   }
 
 
-  /////////////////////////////
-  // Selection and crosshair //
-  /////////////////////////////
-
-  // Shared crosshair and tooltip
-  appEvents.on(
-    'graph-hover',
-    event => {
-      drawSharedCrosshair(event.pos);
-    },
-    scope
-  );
-
-  appEvents.on(
-    'graph-hover-clear',
-    () => {
-      clearCrosshair();
-    },
-    scope
-  );
+  onMouseDown(event) {
+    this.selection.active = true;
+    this.selection.x1 = event.offsetX;
 
 
-  function onMouseDown(event) {
-    selection.active = true;
-    selection.x1 = event.offsetX;
-
-    mouseUpHandler = function() {
-      onMouseUp();
+    this.mouseUpHandler = () => {
+      this.onMouseUp();
     };
     };
 
 
-    $(document).one('mouseup', mouseUpHandler);
+    $(document).one('mouseup', this.mouseUpHandler);
   }
   }
 
 
-  function onMouseUp() {
-    $(document).unbind('mouseup', mouseUpHandler);
-    mouseUpHandler = null;
-    selection.active = false;
+  onMouseUp() {
+    $(document).unbind('mouseup', this.mouseUpHandler);
+    this.mouseUpHandler = null;
+    this.selection.active = false;
 
 
-    let selectionRange = Math.abs(selection.x2 - selection.x1);
-    if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
-      let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth);
-      let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth);
+    let selectionRange = Math.abs(this.selection.x2 - this.selection.x1);
+    if (this.selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
+      let timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth);
+      let timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth);
 
 
-      ctrl.timeSrv.setTime({
+      this.ctrl.timeSrv.setTime({
         from: moment.utc(timeFrom),
         from: moment.utc(timeFrom),
         to: moment.utc(timeTo),
         to: moment.utc(timeTo),
       });
       });
     }
     }
 
 
-    clearSelection();
+    this.clearSelection();
   }
   }
 
 
-  function onMouseLeave() {
+  onMouseLeave() {
     appEvents.emit('graph-hover-clear');
     appEvents.emit('graph-hover-clear');
-    clearCrosshair();
+    this.clearCrosshair();
   }
   }
 
 
-  function onMouseMove(event) {
-    if (!heatmap) {
+  onMouseMove(event) {
+    if (!this.heatmap) {
       return;
       return;
     }
     }
 
 
-    if (selection.active) {
+    if (this.selection.active) {
       // Clear crosshair and tooltip
       // Clear crosshair and tooltip
-      clearCrosshair();
-      tooltip.destroy();
+      this.clearCrosshair();
+      this.tooltip.destroy();
 
 
-      selection.x2 = limitSelection(event.offsetX);
-      drawSelection(selection.x1, selection.x2);
+      this.selection.x2 = this.limitSelection(event.offsetX);
+      this.drawSelection(this.selection.x1, this.selection.x2);
     } else {
     } else {
-      emitGraphHoverEvent(event);
-      drawCrosshair(event.offsetX);
-      tooltip.show(event, data);
+      this.emitGraphHoverEvent(event);
+      this.drawCrosshair(event.offsetX);
+      this.tooltip.show(event, this.data);
     }
     }
   }
   }
 
 
-  function emitGraphHoverEvent(event) {
-    let x = xScale.invert(event.offsetX - yAxisWidth).valueOf();
-    let y = yScale.invert(event.offsetY);
+  emitGraphHoverEvent(event) {
+    let x = this.xScale.invert(event.offsetX - this.yAxisWidth).valueOf();
+    let y = this.yScale.invert(event.offsetY);
     let pos = {
     let pos = {
       pageX: event.pageX,
       pageX: event.pageX,
       pageY: event.pageY,
       pageY: event.pageY,
@@ -730,105 +743,100 @@ export default function link(scope, elem, attrs, ctrl) {
     };
     };
 
 
     // Set minimum offset to prevent showing legend from another panel
     // Set minimum offset to prevent showing legend from another panel
-    pos.panelRelY = Math.max(event.offsetY / height, 0.001);
+    pos.panelRelY = Math.max(event.offsetY / this.height, 0.001);
 
 
     // broadcast to other graph panels that we are hovering
     // broadcast to other graph panels that we are hovering
-    appEvents.emit('graph-hover', { pos: pos, panel: panel });
+    appEvents.emit('graph-hover', { pos: pos, panel: this.panel });
   }
   }
 
 
-  function limitSelection(x2) {
-    x2 = Math.max(x2, yAxisWidth);
-    x2 = Math.min(x2, chartWidth + yAxisWidth);
+  limitSelection(x2) {
+    x2 = Math.max(x2, this.yAxisWidth);
+    x2 = Math.min(x2, this.chartWidth + this.yAxisWidth);
     return x2;
     return x2;
   }
   }
 
 
-  function drawSelection(posX1, posX2) {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-selection').remove();
+  drawSelection(posX1, posX2) {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-selection').remove();
       let selectionX = Math.min(posX1, posX2);
       let selectionX = Math.min(posX1, posX2);
       let selectionWidth = Math.abs(posX1 - posX2);
       let selectionWidth = Math.abs(posX1 - posX2);
 
 
       if (selectionWidth > MIN_SELECTION_WIDTH) {
       if (selectionWidth > MIN_SELECTION_WIDTH) {
-        heatmap
+        this.heatmap
           .append('rect')
           .append('rect')
           .attr('class', 'heatmap-selection')
           .attr('class', 'heatmap-selection')
           .attr('x', selectionX)
           .attr('x', selectionX)
           .attr('width', selectionWidth)
           .attr('width', selectionWidth)
-          .attr('y', chartTop)
-          .attr('height', chartHeight);
+          .attr('y', this.chartTop)
+          .attr('height', this.chartHeight);
       }
       }
     }
     }
   }
   }
 
 
-  function clearSelection() {
-    selection.x1 = -1;
-    selection.x2 = -1;
+  clearSelection() {
+    this.selection.x1 = -1;
+    this.selection.x2 = -1;
 
 
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-selection').remove();
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-selection').remove();
     }
     }
   }
   }
 
 
-  function drawCrosshair(position) {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-crosshair').remove();
+  drawCrosshair(position) {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-crosshair').remove();
 
 
       let posX = position;
       let posX = position;
-      posX = Math.max(posX, yAxisWidth);
-      posX = Math.min(posX, chartWidth + yAxisWidth);
+      posX = Math.max(posX, this.yAxisWidth);
+      posX = Math.min(posX, this.chartWidth + this.yAxisWidth);
 
 
-      heatmap
+      this.heatmap
         .append('g')
         .append('g')
         .attr('class', 'heatmap-crosshair')
         .attr('class', 'heatmap-crosshair')
         .attr('transform', 'translate(' + posX + ',0)')
         .attr('transform', 'translate(' + posX + ',0)')
         .append('line')
         .append('line')
         .attr('x1', 1)
         .attr('x1', 1)
-        .attr('y1', chartTop)
+        .attr('y1', this.chartTop)
         .attr('x2', 1)
         .attr('x2', 1)
-        .attr('y2', chartBottom)
+        .attr('y2', this.chartBottom)
         .attr('stroke-width', 1);
         .attr('stroke-width', 1);
     }
     }
   }
   }
 
 
-  function drawSharedCrosshair(pos) {
-    if (heatmap && ctrl.dashboard.graphTooltip !== 0) {
-      let posX = xScale(pos.x) + yAxisWidth;
-      drawCrosshair(posX);
+  drawSharedCrosshair(pos) {
+    if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) {
+      let posX = this.xScale(pos.x) + this.yAxisWidth;
+      this.drawCrosshair(posX);
     }
     }
   }
   }
 
 
-  function clearCrosshair() {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-crosshair').remove();
+  clearCrosshair() {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-crosshair').remove();
     }
     }
   }
   }
 
 
-  function render() {
-    data = ctrl.data;
-    panel = ctrl.panel;
-    timeRange = ctrl.range;
+  render() {
+    this.data = this.ctrl.data;
+    this.panel = this.ctrl.panel;
+    this.timeRange = this.ctrl.range;
 
 
-    if (!setElementHeight() || !data) {
+    if (!this.setElementHeight() || !this.data) {
       return;
       return;
     }
     }
 
 
     // Draw default axes and return if no data
     // Draw default axes and return if no data
-    if (_.isEmpty(data.buckets)) {
-      addHeatmapCanvas();
-      addAxes();
+    if (_.isEmpty(this.data.buckets)) {
+      this.addHeatmapCanvas();
+      this.addAxes();
       return;
       return;
     }
     }
 
 
-    addHeatmap();
-    scope.yAxisWidth = yAxisWidth;
-    scope.xAxisHeight = xAxisHeight;
-    scope.chartHeight = chartHeight;
-    scope.chartWidth = chartWidth;
-    scope.chartTop = chartTop;
+    this.addHeatmap();
+    this.scope.yAxisWidth = this.yAxisWidth;
+    this.scope.xAxisHeight = this.xAxisHeight;
+    this.scope.chartHeight = this.chartHeight;
+    this.scope.chartWidth = this.chartWidth;
+    this.scope.chartTop = this.chartTop;
   }
   }
-
-  // Register selection listeners
-  $heatmap.on('mousedown', onMouseDown);
-  $heatmap.on('mousemove', onMouseMove);
-  $heatmap.on('mouseleave', onMouseLeave);
 }
 }