function drawContextElements(cxtGroup, cxtWidth, cxtChartHeight, swlHeight) {
      const data = scope.contextChartData;

      contextXScale = d3.time.scale().range([0, cxtWidth])
        .domain(calculateContextXAxisDomain());

      const combinedData = scope.contextForecastData === undefined ? data : data.concat(scope.contextForecastData);
      const valuesRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE };
      _.each(combinedData, (item) => {
        valuesRange.min = Math.min(item.value, valuesRange.min);
        valuesRange.max = Math.max(item.value, valuesRange.max);
      });
      let dataMin = valuesRange.min;
      let dataMax = valuesRange.max;
      const chartLimits = { min: dataMin, max: dataMax };

      if (scope.modelPlotEnabled === true ||
        (scope.contextForecastData !== undefined && scope.contextForecastData.length > 0)) {
        const boundsRange = { min: Number.MAX_VALUE, max: Number.MIN_VALUE };
        _.each(combinedData, (item) => {
          boundsRange.min = Math.min(item.lower, boundsRange.min);
          boundsRange.max = Math.max(item.upper, boundsRange.max);
        });
        dataMin = Math.min(dataMin, boundsRange.min);
        dataMax = Math.max(dataMax, boundsRange.max);

        // Set the y axis domain so that the range of actual values takes up at least 50% of the full range.
        if ((valuesRange.max - valuesRange.min) < 0.5 * (dataMax - dataMin)) {
          if (valuesRange.min > dataMin) {
            chartLimits.min = valuesRange.min - (0.5 * (valuesRange.max - valuesRange.min));
          }

          if (valuesRange.max < dataMax) {
            chartLimits.max = valuesRange.max + (0.5 * (valuesRange.max - valuesRange.min));
          }
        }
      }

      contextYScale = d3.scale.linear().range([cxtChartHeight, contextChartLineTopMargin])
        .domain([chartLimits.min, chartLimits.max]);

      const borders = cxtGroup.append('g')
        .attr('class', 'axis');

      // Add borders left and right.
      borders.append('line')
        .attr('x1', 0)
        .attr('y1', 0)
        .attr('x2', 0)
        .attr('y2', cxtChartHeight + swlHeight);
      borders.append('line')
        .attr('x1', cxtWidth)
        .attr('y1', 0)
        .attr('x2', cxtWidth)
        .attr('y2', cxtChartHeight + swlHeight);

      // Add x axis.
      const bounds = timefilter.getActiveBounds();
      const timeBuckets = new TimeBuckets();
      timeBuckets.setInterval('auto');
      timeBuckets.setBounds(bounds);
      const xAxisTickFormat = timeBuckets.getScaledDateFormat();
      const xAxis = d3.svg.axis().scale(contextXScale)
        .orient('top')
        .innerTickSize(-cxtChartHeight)
        .outerTickSize(0)
        .tickPadding(0)
        .ticks(numTicksForDateFormat(cxtWidth, xAxisTickFormat))
        .tickFormat((d) => {
          return moment(d).format(xAxisTickFormat);
        });

      cxtGroup.datum(data);

      const contextBoundsArea = d3.svg.area()
        .x((d) => { return contextXScale(d.date); })
        .y0((d) => { return contextYScale(Math.min(chartLimits.max, Math.max(d.lower, chartLimits.min))); })
        .y1((d) => { return contextYScale(Math.max(chartLimits.min, Math.min(d.upper, chartLimits.max))); })
        .defined(d => (d.lower !== null && d.upper !== null));

      if (scope.modelPlotEnabled === true) {
        cxtGroup.append('path')
          .datum(data)
          .attr('class', 'area context')
          .attr('d', contextBoundsArea);
      }

      const contextValuesLine = d3.svg.line()
        .x((d) => { return contextXScale(d.date); })
        .y((d) => { return contextYScale(d.value); })
        .defined(d => d.value !== null);

      cxtGroup.append('path')
        .datum(data)
        .attr('class', 'values-line')
        .attr('d', contextValuesLine);
      drawLineChartDots(data, cxtGroup, contextValuesLine, 1);

      // Create the path elements for the forecast value line and bounds area.
      if (scope.contextForecastData !== undefined) {
        cxtGroup.append('path')
          .datum(scope.contextForecastData)
          .attr('class', 'area forecast')
          .attr('d', contextBoundsArea);
        cxtGroup.append('path')
          .datum(scope.contextForecastData)
          .attr('class', 'values-line forecast')
          .attr('d', contextValuesLine);
      }

      // Create and draw the anomaly swimlane.
      const swimlane = cxtGroup.append('g')
        .attr('class', 'swimlane')
        .attr('transform', 'translate(0,' + cxtChartHeight + ')');

      drawSwimlane(swimlane, cxtWidth, swlHeight);

      // Draw a mask over the sections of the context chart and swimlane
      // which fall outside of the zoom brush selection area.
      mask = new ContextChartMask(cxtGroup, scope.contextChartData, scope.modelPlotEnabled, swlHeight)
        .x(contextXScale)
        .y(contextYScale);

      // Draw the x axis on top of the mask so that the labels are visible.
      cxtGroup.append('g')
        .attr('class', 'x axis context-chart-axis')
        .call(xAxis);

      // Move the x axis labels up so that they are inside the contact chart area.
      cxtGroup.selectAll('.x.context-chart-axis text')
        .attr('dy', (cxtChartHeight - 5));

      filterAxisLabels(cxtGroup.selectAll('.x.context-chart-axis'), cxtWidth);

      drawContextBrush(cxtGroup);
    }
    function renderFocusChart() {
      if (scope.focusChartData === undefined) {
        return;
      }

      const data = scope.focusChartData;

      const focusChart = d3.select('.focus-chart');

      // Update the plot interval labels.
      const focusAggInt = scope.focusAggregationInterval.expression;
      const bucketSpan = scope.selectedJob.analysis_config.bucket_span;
      angular.element('.zoom-aggregation-interval').text(
        `(aggregation interval: ${focusAggInt}, bucket span: ${bucketSpan})`);

      // Render the axes.

      // Calculate the x axis domain.
      // Elasticsearch aggregation returns points at start of bucket,
      // so set the x-axis min to the start of the first aggregation interval,
      // and the x-axis max to the end of the last aggregation interval.
      const bounds = scope.selectedBounds;
      const aggMs = scope.focusAggregationInterval.asMilliseconds();
      const earliest = moment(Math.floor((bounds.min.valueOf()) / aggMs) * aggMs);
      const latest = moment(Math.ceil((bounds.max.valueOf()) / aggMs) * aggMs);
      focusXScale.domain([earliest.toDate(), latest.toDate()]);

      // Calculate the y-axis domain.
      if (scope.focusChartData.length > 0 ||
        (scope.focusForecastData !== undefined && scope.focusForecastData.length > 0)) {
        if (fieldFormat !== undefined) {
          focusYAxis.tickFormat(d => fieldFormat.convert(d, 'text'));
        } else {
          // Use default tick formatter.
          focusYAxis.tickFormat(null);
        }

        // Calculate the min/max of the metric data and the forecast data.
        let yMin = 0;
        let yMax = 0;

        let combinedData = data;
        if (scope.focusForecastData !== undefined && scope.focusForecastData.length > 0) {
          combinedData = data.concat(scope.focusForecastData);
        }

        yMin = d3.min(combinedData, (d) => {
          return d.lower !== undefined ? Math.min(d.value, d.lower) : d.value;
        });
        yMax = d3.max(combinedData, (d) => {
          return d.upper !== undefined ? Math.max(d.value, d.upper) : d.value;
        });

        if (yMax === yMin) {
          if (
            contextYScale.domain()[0] !== contextYScale.domain()[1] &&
            yMin >= contextYScale.domain()[0] && yMax <= contextYScale.domain()[1]
          ) {
            // Set the focus chart limits to be the same as the context chart.
            yMin = contextYScale.domain()[0];
            yMax = contextYScale.domain()[1];
          } else {
            yMin -= (yMin * 0.05);
            yMax += (yMax * 0.05);
          }
        }

        focusYScale.domain([yMin, yMax]);

      } else {
        // Display 10 unlabelled ticks.
        focusYScale.domain([0, 10]);
        focusYAxis.tickFormat('');
      }

      // Get the scaled date format to use for x axis tick labels.
      const timeBuckets = new TimeBuckets();
      timeBuckets.setInterval('auto');
      timeBuckets.setBounds(bounds);
      const xAxisTickFormat = timeBuckets.getScaledDateFormat();
      focusChart.select('.x.axis')
        .call(focusXAxis.ticks(numTicksForDateFormat(vizWidth), xAxisTickFormat)
          .tickFormat((d) => {
            return moment(d).format(xAxisTickFormat);
          }));
      focusChart.select('.y.axis')
        .call(focusYAxis);

      filterAxisLabels(focusChart.select('.x.axis'), vizWidth);

      // Render the bounds area and values line.
      if (scope.modelPlotEnabled === true) {
        focusChart.select('.area.bounds')
          .attr('d', focusBoundedArea(data))
          .classed('hidden', !scope.showModelBounds);
      }

      focusChart.select('.values-line')
        .attr('d', focusValuesLine(data));
      drawLineChartDots(data, focusChart, focusValuesLine);

      // Render circle markers for the points.
      // These are used for displaying tooltips on mouseover.
      // Don't render dots where value=null (data gaps) or for multi-bucket anomalies.
      const dots = d3.select('.focus-chart-markers').selectAll('.metric-value')
        .data(data.filter(d => (d.value !== null && !showMultiBucketAnomalyMarker(d))));

      // Remove dots that are no longer needed i.e. if number of chart points has decreased.
      dots.exit().remove();
      // Create any new dots that are needed i.e. if number of chart points has increased.
      dots.enter().append('circle')
        .attr('r', LINE_CHART_ANOMALY_RADIUS)
        .on('mouseover', function (d) {
          showFocusChartTooltip(d, this);
        })
        .on('mouseout', () => mlChartTooltipService.hide());

      // Update all dots to new positions.
      dots.attr('cx', (d) => { return focusXScale(d.date); })
        .attr('cy', (d) => { return focusYScale(d.value); })
        .attr('class', (d) => {
          let markerClass = 'metric-value';
          if (_.has(d, 'anomalyScore')) {
            markerClass += ` anomaly-marker ${getSeverityWithLow(d.anomalyScore)}`;
          }
          return markerClass;
        });


      // Render cross symbols for any multi-bucket anomalies.
      const multiBucketMarkers = d3.select('.focus-chart-markers').selectAll('.multi-bucket')
        .data(data.filter(d => (d.anomalyScore !== null && showMultiBucketAnomalyMarker(d) === true)));

      // Remove multi-bucket markers that are no longer needed.
      multiBucketMarkers.exit().remove();

      // Add any new markers that are needed i.e. if number of multi-bucket points has increased.
      multiBucketMarkers.enter().append('path')
        .attr('d', d3.svg.symbol().size(MULTI_BUCKET_SYMBOL_SIZE).type('cross'))
        .on('mouseover', function (d) {
          showFocusChartTooltip(d, this);
        })
        .on('mouseout', () => mlChartTooltipService.hide());

      // Update all markers to new positions.
      multiBucketMarkers.attr('transform', d => `translate(${focusXScale(d.date)}, ${focusYScale(d.value)})`)
        .attr('class', d => `anomaly-marker multi-bucket ${getSeverityWithLow(d.anomalyScore)}`);


      // Add rectangular markers for any scheduled events.
      const scheduledEventMarkers = d3.select('.focus-chart-markers').selectAll('.scheduled-event-marker')
        .data(data.filter(d => d.scheduledEvents !== undefined));

      // Remove markers that are no longer needed i.e. if number of chart points has decreased.
      scheduledEventMarkers.exit().remove();

      // Create any new markers that are needed i.e. if number of chart points has increased.
      scheduledEventMarkers.enter().append('rect')
        .attr('width', LINE_CHART_ANOMALY_RADIUS * 2)
        .attr('height', SCHEDULED_EVENT_SYMBOL_HEIGHT)
        .attr('class', 'scheduled-event-marker')
        .attr('rx', 1)
        .attr('ry', 1);

      // Update all markers to new positions.
      scheduledEventMarkers.attr('x', (d) => focusXScale(d.date) - LINE_CHART_ANOMALY_RADIUS)
        .attr('y', (d) => focusYScale(d.value) - 3);

      // Plot any forecast data in scope.
      if (scope.focusForecastData !== undefined) {
        focusChart.select('.area.forecast')
          .attr('d', focusBoundedArea(scope.focusForecastData))
          .classed('hidden', !scope.showForecast);
        focusChart.select('.values-line.forecast')
          .attr('d', focusValuesLine(scope.focusForecastData))
          .classed('hidden', !scope.showForecast);

        const forecastDots = d3.select('.focus-chart-markers.forecast').selectAll('.metric-value')
          .data(scope.focusForecastData);

        // Remove dots that are no longer needed i.e. if number of forecast points has decreased.
        forecastDots.exit().remove();
        // Create any new dots that are needed i.e. if number of forecast points has increased.
        forecastDots.enter().append('circle')
          .attr('r', LINE_CHART_ANOMALY_RADIUS)
          .on('mouseover', function (d) {
            showFocusChartTooltip(d, this);
          })
          .on('mouseout', () => mlChartTooltipService.hide());

        // Update all dots to new positions.
        forecastDots.attr('cx', (d) => { return focusXScale(d.date); })
          .attr('cy', (d) => { return focusYScale(d.value); })
          .attr('class', 'metric-value')
          .classed('hidden', !scope.showForecast);
      }

    }