Source: src/js/views/MetricsChartView.js

define(["jquery", "underscore", "backbone", "d3"], function (
  $,
  _,
  Backbone,
  d3,
) {
  "use strict";

  /**
   * @class MetricsChartView
   * @classdesc The MetricsChartView will render an SVG times-series chart using D3 that shows the number of metrics over time.
   * @screenshot views/MetricsChartView.png
   * @classcategory Views
   * @extends Backbone.View
   */
  var MetricsChartView = Backbone.View.extend(
    /** @lends MetricsChartView.prototype */ {
      initialize: function (options) {
        if (!d3) {
          console.log("SVG is not supported");
          return null;
        }

        if (typeof options !== "undefined") {
          this.model = options.model || null; // TODO: figure out how to set the model on this view

          this.metricCount = options.metricCount || "0"; // for now, use individual arrays
          this.metricMonths = options.metricMonths || "0";
          this.id = options.id || "metrics-chart";
          this.viewType = options.type || "dataset";
          this.width = options.width || 600;
          this.height = options.height || 370;
          this.metricName = options.metricName;
        }
      },

      // http://stackoverflow.com/questions/9651167/svg-not-rendering-properly-as-a-backbone-view
      // Give our el a svg namespace because Backbone gives a different one automatically
      nameSpace: "http://www.w3.org/2000/svg",
      _ensureElement: function () {
        if (!this.el) {
          var attrs = _.extend({}, _.result(this, "attributes"));
          if (this.id) attrs.id = _.result(this, "id");
          if (this.className) attrs["class"] = _.result(this, "className");
          var $el = $(
            window.document.createElementNS(
              _.result(this, "nameSpace"),
              _.result(this, "tagName"),
            ),
          ).attr(attrs);
          this.setElement($el, false);
        } else {
          this.setElement(_.result(this, "el"), false);
        }
      },

      tagName: "svg",

      /** Renders this Metric Chart view. */
      render: function () {
        //Clear out any view elements in case this is a re-render
        this.$el.empty();

        /*
          * ========================================================================
          *  NAMING CONVENTIONS:

          CONTEXT: Context refers to the mini slider chart at the bottom, that includes the d3 "brush"
          FOCUS: Focus refers to the larger main chart at the top
          BRUSH: The rectangle in the context chart that highlights what is currently in focus in the focus chart.

          * ========================================================================
          */

        // check if there have been any views/citations
        var sumMetricCount = 0;
        for (var i = 0; i < this.metricCount.length; i++) {
          sumMetricCount += this.metricCount[i];
        }

        var self = this;

        // when ther no data or no views/citations yet, just show some text:
        if (
          this.metricCount.length == 0 ||
          this.metricCount == 0 ||
          sumMetricCount == 0
        ) {
          var metricNameLemma = this.metricName
            .toLowerCase()
            .substring(0, this.metricName.length - 1);
          var textMessage =
            "This dataset hasn’t been " + metricNameLemma + "ed yet.";
          if (this.viewType != "dataset") {
            textMessage =
              "These datasets have not been " + metricNameLemma + "ed yet.";
          }

          var margin = { top: 25, right: 40, bottom: 40, left: 40 },
            height = this.height - margin.top - margin.bottom;

          //Set the chart width
          this.$el.css("width", "100%");
          var width =
            (this.$el.width() || this.width) - margin.left - margin.right;
          this.width = width;

          var vis = d3
            .select(this.el)
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .attr("class", "line-chart no-data");

          var bkg = vis
            .append("svg:rect")
            .attr("class", "no-data")
            .attr("width", width)
            .attr("height", height)
            .attr("rx", 2)
            .attr("ry", 2)
            .attr(
              "transform",
              "translate(" + margin.left + "," + margin.top + ")",
            );

          var msg = vis
            .append("text")
            .attr("class", "no-data")
            .text(textMessage)
            .attr("text-anchor", "middle")
            .attr("font-size", "20px")
            .attr("x", width / 2)
            .attr("y", height / 2)
            .attr(
              "transform",
              "translate(" + margin.left + "," + margin.top + ")",
            );

          // if there is data (even a series of zeros), draw the time-series chart:
        } else {
          /*
           * ========================================================================
           *  Global variables and options
           * ========================================================================
           */

          var metricName = this.metricName;

          // the format of the date in the input data
          var input_date_format = d3.time.format("%Y-%m");
          // how dates will be displayed in the chart in most cases
          var display_date_format = d3.time.format("%b %Y");

          // the length of a day/year in milliseconds
          var day_in_ms = 86400000,
            year_in_ms = 31540000000;

          // focus chart sizing
          var margin = { top: 30, right: 30, bottom: 95, left: 20 },
            height = this.height - margin.top - margin.bottom;

          //Set the chart width
          this.$el.css("width", "100%");
          var width =
            (this.$el.width() || this.width) - margin.left - margin.right;
          this.width = width;

          // context chart sizing
          var margin_context = { top: 315, right: 30, bottom: 20, left: 20 },
            height_context =
              this.height - margin_context.top - margin_context.bottom;

          // zoom button sizing
          var button_width = 40,
            button_height = 14,
            button_padding = 10;

          // how wide does the tooltip div need to be to accomdate text?
          var tooltip_width = 76;

          // what proportion of a month should a bar cover?
          var bar_width_factor = 0.8;

          /*
           * ========================================================================
           *  Prepare data
           * ========================================================================
           */

          // change dates to milliseconds, to enable calculating the `d3.extent`
          var metricMonths_parsed = [];
          this.metricMonths.forEach(function (part, index, theArray) {
            try {
              metricMonths_parsed[index] = input_date_format
                .parse(part)
                .getTime();
            } catch {
              // replace null with current month
              var today = new Date();
              var yyyy = today.getFullYear();
              var mm = today.getMonth() + 1;
              var updatedPart =
                yyyy.toString() + "-" + mm.toString().padStart(2, "0");

              metricMonths_parsed[index] = input_date_format
                .parse(updatedPart)
                .getTime();
            }
          });

          // input data from model doesn't list months where there were 0 counts for all metrics (views/downloads/citations)
          // construct an array of all months between min and max dates to use as x variable
          var all_months = d3.time
            .scale()
            .domain(d3.extent(metricMonths_parsed))
            .ticks(d3.time.months, 1);

          // add padding to both sides of array so that bars don't get cut off.
          // add more padding when there's just one bar (otherwise it's too wide)
          if (metricMonths_parsed.length == 1) {
            var new_min_date = new Date(
                d3.extent(metricMonths_parsed)[0] - day_in_ms * 13,
              ),
              new_max_date = new Date(
                d3.extent(metricMonths_parsed)[1] +
                  day_in_ms * 31 * bar_width_factor +
                  day_in_ms * 13,
              );
          } else {
            var new_min_date = new Date(
                d3.extent(metricMonths_parsed)[0] - day_in_ms * 1,
              ),
              new_max_date = new Date(
                d3.extent(metricMonths_parsed)[1] +
                  day_in_ms * 31 * bar_width_factor,
              );
          }

          all_months.push(new_min_date);
          // also add a little padding on the left for consistency
          all_months.push(new_max_date);

          // for each month, check whether there is a count available,
          // if so append it, otherwise append zero.
          var dataset = [];
          for (var i = 0; i < all_months.length; i++) {
            var match_index = metricMonths_parsed.indexOf(
              all_months[i].getTime(),
            );
            if (match_index == -1) {
              // no match in data
              dataset.push({ integer: i, month: all_months[i], count: 0 });
            } else {
              // match in data
              dataset.push({
                integer: i,
                month: all_months[i],
                count: this.metricCount[match_index],
              });
            }
          }

          /*
           * ========================================================================
           *  x and y coordinates
           * ========================================================================
           */

          var x_full_extent = d3.extent(dataset, function (d) {
            return d.month;
          });
          var bar_width =
            ((day_in_ms * 30) / (x_full_extent[1] - x_full_extent[0])) *
            width *
            bar_width_factor;

          /* === Focus Chart === */

          var x = d3.time
            .scale()
            .range([0, width])
            .domain(
              d3.extent(dataset, function (d) {
                return d.month;
              }),
            );

          var y = d3.scale
            .linear()
            .range([height, 0])
            .domain([
              0,
              d3.max(dataset, function (d) {
                return d.count;
              }) * 1.04,
            ]);

          var x_axis = d3.svg
            .axis()
            .scale(x)
            .orient("bottom")
            .tickSize(-height)
            .ticks(generate_ticks)
            .tickFormat(format_months);

          var y_axis = d3.svg
            .axis()
            .scale(y)
            .ticks(4)
            .tickFormat(d3.format("d"))
            .tickSize(-width)
            .orient("right");

          /* === Context Chart === */

          var x_context = d3.time
            .scale()
            .range([0, width])
            .domain(
              d3.extent(dataset, function (d) {
                return d.month;
              }),
            );

          var y_context = d3.scale
            .linear()
            .range([height_context, 0])
            .domain(y.domain());

          var x_axis_context = d3.svg
            .axis()
            .scale(x_context)
            .orient("bottom")
            .ticks(generate_ticks)
            .tickFormat(format_months);

          /*
           * ========================================================================
           *  Variables for brushing and zooming behaviour
           * ========================================================================
           */

          var brush = d3.svg
            .brush()
            .x(x_context)
            .on("brush", change_focus_brush)
            .on("brushend", check_bounds);

          var zoom = d3.behavior
            .zoom()
            .on("zoom", change_focus_zoom)
            .on("zoomend", check_bounds);

          /*
           * ========================================================================
           *  Define the SVG area ("vis") and append all the layers
           * ========================================================================
           */

          // === the main components === //

          var vis = d3
            .select(this.el)
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
            .attr("class", "line-chart");

          // clipPath is used to keep elements from moving outside of plot area when viwer zooms/scrolls/brushes
          vis
            .append("defs")
            .append("clipPath")
            .attr("id", "clip")
            .append("rect")
            .attr("width", width)
            .attr("height", height);

          var pane = vis
            .append("rect")
            .attr("class", "pane")
            .attr("width", width)
            .attr("height", height)
            .attr(
              "transform",
              "translate(" + margin.left + "," + margin.top + ")",
            );

          var context = vis
            .append("g")
            .attr("class", "context")
            .attr(
              "transform",
              "translate(" +
                margin_context.left +
                "," +
                margin_context.top +
                ")",
            );

          var focus = vis
            .append("g")
            .attr("class", "focus")
            .attr(
              "transform",
              "translate(" + margin.left + "," + margin.top + ")",
            );

          // === current date range text & zoom buttons === //

          var expl_text = vis
            .append("g")
            .attr("id", "buttons_group")
            .attr("transform", "translate(" + 0 + "," + 0 + ")");

          expl_text
            .append("text")
            .attr("id", "totalCount")
            .style("text-anchor", "start")
            .attr("transform", "translate(" + 18 + "," + 11 + ")");

          expl_text
            .append("text")
            .attr("id", "displayDates")
            .style("text-anchor", "start")
            .attr("transform", "translate(" + 20 + "," + 22 + ")");

          update_context();

          // === the zooming/scaling buttons === //

          if (x_full_extent[1] - x_full_extent[0] < year_in_ms) {
            var button_data = ["month", "all"];
          } else {
            var button_data = ["year", "month", "all"];
          }

          var button_count = button_data.length - 1,
            button_g_width =
              button_count * button_width +
              button_count * button_padding +
              margin.right -
              button_padding;

          expl_text
            .append("text")
            .attr("class", "zoomto_text")
            .text("Zoom to")
            .style("text-anchor", "start")
            .attr(
              "transform",
              "translate(" + (width - button_g_width - 45) + "," + 14 + ")",
            )
            .style("opacity", "0");

          var button = expl_text
            .selectAll("g")
            .data(button_data)
            .enter()
            .append("g")
            .attr("class", "scale_button")
            .attr("transform", function (d, i) {
              return (
                "translate(" +
                (width -
                  button_g_width +
                  i * button_width +
                  i * button_padding) +
                ",4)"
              );
            })
            .style("opacity", "0");

          button
            .append("rect")
            .attr("class", "button_rect")
            .attr("width", button_width)
            .attr("height", button_height)
            .attr("rx", 1)
            .attr("ry", 1);

          button
            .append("text")
            .attr("dy", button_height / 2 + 3)
            .attr("dx", button_width / 2)
            .style("text-anchor", "middle")
            .text(function (d) {
              return d;
            });

          /* === focus chart === */

          focus
            .append("g")
            .attr("class", "y axis")
            .call(y_axis)
            .attr("transform", "translate(" + width + ", 0)")
            .style("text-anchor", "middle");

          // x-axis
          focus
            .append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + height + ")")
            .call(x_axis)
            .style("text-anchor", "middle");

          // enter bars
          focus
            .selectAll(".bar")
            .data(dataset)
            .enter()
            .append("rect")
            .attr("class", "bar")
            .attr("id", function (d) {
              return "bar_" + d.month.getTime();
            }) //id of each bar is "bar_" plus it's associated date in ms
            .attr("x", function (d) {
              return x(d.month);
            })
            .attr("y", height)
            .attr("height", 0)
            .attr("width", bar_width)
            .style("opacity", 0)
            .on("mouseover", function (d) {
              var floor_month = d3.time.month.floor(d.month).getTime();
              highlight_bar("#bar_" + floor_month);
              highlight_label("#label_" + floor_month);
              show_tooltip(d);
            })
            .on("mouseout", function (d) {
              var floor_month = d3.time.month.floor(d.month).getTime();
              unhighlight_bar("#bar_" + floor_month);
              unhighlight_label("#label_" + floor_month);
              hide_tooltip(d);
            });

          // animate bars
          focus
            .selectAll(".bar")
            .transition()
            .duration(450)
            .ease("elastic", 1.03, 0.98)
            .delay(function (d, i) {
              var max_delay = 600;
              var z = i / (dataset.length - 1);
              var line_z = z * max_delay * 0.4;
              var log_z = Math.log2(z + 1) * max_delay * 0.6;
              return 250 + line_z + log_z;
            })
            .attr("y", function (d) {
              return y(d.count);
            })
            .attr("height", function (d) {
              return y(0) - y(d.count);
            })
            .style("opacity", 1);

          /* === context chart === */

          // enter context bars
          context
            .selectAll(".bar_context")
            .data(dataset)
            .enter()
            .append("rect")
            .attr("class", "bar_context")
            .attr("x", function (d) {
              return x_context(d.month);
            })
            .attr("y", height_context)
            .attr("height", 0)
            .attr("width", bar_width)
            .style("opacity", 0);

          // animate context bars
          context
            .selectAll(".bar_context")
            .transition()
            .duration(450)
            .ease("elastic", 1.03, 0.98)
            .delay(function (d, i) {
              var max_delay = 600;
              var z = i / (dataset.length - 1);
              var line_z = z * max_delay * 0.4;
              var log_z = Math.log2(z + 1) * max_delay * 0.6;
              return line_z + log_z;
            })
            .attr("y", function (d) {
              return y_context(d.count);
            })
            .attr("height", function (d) {
              return y_context(0) - y_context(d.count);
            })
            .style("opacity", 1);

          // x-axis
          context
            .append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + height_context + ")")
            .call(x_axis_context);

          /* === brush  === */

          var brushg = context.append("g").attr("class", "x brush").call(brush);

          brushg
            .selectAll(".extent")
            .attr("y", -6)
            .attr("height", height_context + 8)
            .style("opacity", "0");

          brushg
            .selectAll(".resize")
            .append("rect")
            .attr("class", "handle")
            .attr("transform", "translate(0," + -3 + ")")
            .attr("rx", 1)
            .attr("ry", 1)
            .attr("height", 0)
            .attr("width", 3)
            .style("opacity", "0");

          brushg
            .selectAll(".resize")
            .append("rect")
            .attr("class", "handle-mini")
            .attr("transform", "translate(-2,8.5)")
            .attr("rx", 2)
            .attr("ry", 2)
            .attr("height", 0)
            .attr("width", 7)
            .style("opacity", "0");

          /* === y axis title === */
          vis
            .append("text")
            .attr("class", "y axis title")
            .text("Monthly " + this.metricName)
            .attr("x", -((height + margin.top + margin.bottom - 50) / 2))
            .attr("y", 0)
            .attr("dy", "1em")
            .attr("transform", "rotate(-90)")
            .style("text-anchor", "middle");

          // allow zoom, brush, and scale behavior after a small delay,
          // so that user does not interrupt bar entrance animation.
          // show UI elements only once user is able to interact with them.
          setTimeout(function () {
            // disable the zoom behavior on wheel zoom event
            // add behaviours
            pane.call(zoom).call(change_focus_zoom).on("wheel.zoom", null);
            zoom.x(x);

            vis
              .selectAll(".scale_button")
              .style("cursor", "pointer")
              .on("click", zoom_to_interval);

            // fade in buttons
            vis
              .selectAll(".scale_button,.zoomto_text")
              .transition()
              .duration(100)
              .ease("cubic")
              .style("opacity", "1");

            // fade in brush elements
            brushg
              .selectAll(".extent")
              .transition()
              .duration(100)
              .ease("cubic")
              .style("opacity", "1");

            brushg
              .selectAll(".handle-mini")
              .transition()
              .duration(170)
              .ease("linear")
              .attr("height", height_context / 2)
              .style("opacity", "1");

            brushg
              .selectAll(".handle")
              .transition()
              .duration(170)
              .ease("linear")
              .attr("height", height_context + 6)
              .style("opacity", "1");

            if (self.viewType === "dataset") {
              d3.select(".metric-chart")
                .append("div")
                .attr("class", "metric_tooltip")
                .style("opacity", 0)
                .style("width", tooltip_width + "px");
            } else {
              d3.select("#user-" + self.id)
                .append("div")
                .attr("class", "metric_tooltip")
                .style("opacity", 0)
                .style("width", tooltip_width + "px");
            }
          }, 900);

          /*
           * ========================================================================
           *  Functions
           * ========================================================================
           */

          /*	------------------------------------------------------
          		HELPER FUNCTIONS
          	------------------------------------------------------  */

          function get_zoom_scale() {
            // custom zoom scale needed to calculate width of bars with zoom/brush.
            // can't use zoom.scale() because this needs to be reset (to one) when using change_focus_brush()
            var x_current_width = x.domain()[1] - x.domain()[0],
              x_total_width = x_full_extent[1] - x_full_extent[0],
              zoom_scale = x_total_width / x_current_width;
            return zoom_scale;
          }

          function convert_metric_name(n) {
            // remove s from metric name if count is 1
            if (n == 1) {
              return metricName.slice(0, -1);
            } else {
              return metricName;
            }
          }

          /*	------------------------------------------------------
          		HOVER BEHAVIOUR: X-AXIS LABELS, BARS, TOOLTIPS
          	------------------------------------------------------  */

          function highlight_bar(bar_id) {
            // mouseover effect on bar
            focus
              .select(bar_id)
              .style("stroke-width", "1")
              .style("opacity", "0.9");
          }

          function unhighlight_bar(bar_id) {
            // undo mouseover effect on bar
            focus
              .select(bar_id)
              .style("stroke-width", "0")
              .style("opacity", "1");
          }

          function highlight_label(label_id) {
            // mouseover effect on label
            focus
              .select(label_id)
              .selectAll("text")
              .style("font-weight", "bold");
          }

          function unhighlight_label(label_id) {
            // undo mouseover effect on label
            focus
              .select(label_id)
              .selectAll("text")
              .style("font-weight", "normal");
          }

          function add_tick_behaviour() {
            // adds html ID and hover behaviour to the x-axis ticks/labels
            // this function is called each time these labels/ticks are re-generated.

            focus.selectAll(".x.axis .tick")[0].forEach(function (tick) {
              d3.select(tick)
                .attr("id", function (d, i) {
                  return "label_" + d3.time.month.floor(d).getTime();
                })
                .on("mouseover", function (tick) {
                  // extract the datapoint from dataset that is associated with x-axis label
                  var floor_month = d3.time.month.floor(tick).getTime();
                  var d = dataset.filter(function (d) {
                    return d.month.getTime() === floor_month;
                  })[0];
                  highlight_bar("#bar_" + floor_month);
                  highlight_label("#label_" + floor_month);
                  show_tooltip(d);
                })
                .on("mouseout", function (tick) {
                  var floor_month = d3.time.month.floor(tick).getTime();
                  unhighlight_bar("#bar_" + floor_month);
                  unhighlight_label("#label_" + floor_month);
                  hide_tooltip(tick);
                });
            });
          }

          function show_tooltip(d) {
            if (self.viewType === "dataset") {
              var bar_width_px = bar_width * get_zoom_scale();

              // get the width of the modal. Need for tooltip x-position.
              var modal_width = d3
                .select("#metric-modal")
                .style("width")
                .slice(0, -2);
              var modal_width = Math.round(Number(modal_width));

              d3.select(".metric_tooltip")
                .html(
                  "<b>" +
                    display_date_format(d.month) +
                    "</b><br/>" +
                    d.count +
                    " " +
                    convert_metric_name(d.count),
                )
                .style(
                  "left",
                  x(d.month) +
                    (modal_width - (width + margin.left + margin.right)) +
                    bar_width_px / 2 -
                    tooltip_width / 2 +
                    "px",
                ) //) + 300 + ((width/dataset.length) * 0.5 * get_zoom_scale())) + "px")
                .style("top", y(d.count) + 19 + "px");

              d3.select(".metric_tooltip")
                .transition()
                .duration(60)
                .style("opacity", 0.98);
            } else {
              d3.select("#user-" + self.id + " > .metric_tooltip")
                .html(
                  "<b>" +
                    display_date_format(d.month) +
                    "</b><br/>" +
                    d.count +
                    " " +
                    convert_metric_name(d.count),
                )
                .style("left", d3.event.pageX - 150 + "px")
                .style("top", y(d.count) - y(0) - 150 + "px");

              d3.select("#user-" + self.id + " > .metric_tooltip")
                .transition()
                .duration(60)
                .style("opacity", 0.98);
            }
          }

          function hide_tooltip(d) {
            if (self.viewType === "dataset") {
              d3.select(".metric_tooltip")
                .transition()
                .duration(60)
                .style("opacity", 0);
            } else {
              d3.select("#user-" + self.id + " > .metric_tooltip")
                .transition()
                .duration(60)
                .style("opacity", 0);
            }
          }

          /*	------------------------------------------------------
          		TICK FORMATTING FUNCTIONS (focus x-axis)
          	------------------------------------------------------  */

          function generate_ticks(t0, t1, dt) {
            var label_size_px = 45;
            var max_total_labels = Math.floor(width / label_size_px);
            // offset so that labels are at the center of each month.
            var offset = (day_in_ms * 30 * bar_width_factor) / 2;

            function step(date, next_step) {
              date.setMonth(date.getMonth() + next_step);
            }

            var time = d3.time.month.floor(t0),
              time = new Date(time.getTime() + offset),
              times = [],
              monthFactors = [1, 3, 4, 12];

            while (time < t1) {
              times.push(new Date(+time)), step(time, 1);
            }

            var timesCopy = times;
            var i;

            for (i = 0; times.length > max_total_labels; i++) {
              var times = _.filter(timesCopy, function (d) {
                return d.getMonth() % monthFactors[i] == 0;
              });
            }

            return times;
          }

          function format_months(d) {
            add_tick_behaviour(); // add tick hover behaviour everytime ticks are re-formatted;
            var test = x.domain()[1] - x.domain()[0] > 132167493818; // when to switch from yyyy to mm-yyyy
            if ((d.getMonth() == 0) & test) {
              //if january
              var yearOnly = d3.time.format("%Y");
              return yearOnly(d);
            } else {
              return display_date_format(d);
            }
          }

          /*	------------------------------------------------------
          		BRUSH & ZOOM BEHAVIOUR
          	------------------------------------------------------  */

          function change_focus_brush() {
            // make the x domain match the brush domain
            x.domain(brush.empty() ? x_context.domain() : brush.extent());
            // reset zoom
            zoom.x(x);
            // re-draw axis and elements at new scale
            update_focus();
            // update the explanatory text (total views, date range)
            update_context();
          }

          function change_focus_zoom() {
            // make the brush range change with the x domain in focus
            brush.extent(x.domain());
            vis.select(".brush").call(brush);
            // re-draw axis and elements at new scale
            update_focus();
            // update the explanatory text (total views, date range)
            update_context();
          }

          function update_focus() {
            // calculate where the bar goes out of focus
            var bar_width_days = bar_width_factor * 30.5;

            var left_date = x.domain()[0];
            if (left_date.getDate() < bar_width_days) {
              var left_date = d3.time.month.floor(left_date),
                left_date = new Date(left_date.getTime());
            }

            var data_subset_focus = dataset.filter(function (d) {
              return d.month <= x.domain()[1] && d.month >= left_date;
            });

            var y_max_focus =
              d3.max(data_subset_focus, function (d) {
                return d.count;
              }) * 1.04 || 1.04;
            var y_change_duration = 85;

            // reset y-axis
            y.domain([0, y_max_focus]);
            focus
              .select(".y.axis")
              .transition()
              .duration(y_change_duration * 0.95)
              .call(y_axis);

            // reset bar height given y-axis
            focus
              .selectAll(".bar")
              .transition()
              .duration(y_change_duration)
              .attr("y", function (d) {
                return y(d.count);
              })
              .attr("height", function (d) {
                return y(0) - y(d.count);
              });

            // redraw other elements
            focus.select(".x.axis").call(x_axis);
            focus
              .selectAll(".bar")
              .attr("x", function (d) {
                return x(d.month);
              })
              .attr("width", bar_width * get_zoom_scale())
              .style("opacity", "1"); // incase user scrolls before entrance animation finishes.
          }

          function update_context() {
            // updates display dates, total count, and decreases opacity of context bars out of focus
            var b = brush.extent();

            // calculate where the bar goes out of focus
            var bar_width_days = bar_width_factor * 30.5;
            if (b[0].getDate() >= bar_width_days) {
              var left_date = d3.time.month.ceil(b[0]),
                left_date = new Date(left_date.getTime());
            } else {
              left_date = d3.time.month.floor(b[0]);
            }

            // get the range of data in focus
            // if there's only one data point, make sure start and end month are the same
            if (metricMonths_parsed.length == 1) {
              var start_month = display_date_format(
                  new Date(metricMonths_parsed[0]),
                ),
                end_month = start_month;
            } else {
              var start_month = brush.empty()
                  ? display_date_format(x_full_extent[0])
                  : display_date_format(left_date),
                end_month = brush.empty()
                  ? display_date_format(x_full_extent[1])
                  : display_date_format(b[1]);
            }

            var data_subset_focus = dataset.filter(function (d) {
              if (metricMonths_parsed.length == 1) {
                return d.month;
              } else {
                return (
                  d.month <= display_date_format.parse(end_month) &&
                  d.month >= left_date
                );
              }
            });

            // calcualte the total views/downloads within focus area
            var total_count = 0;
            for (var i = 0; i < data_subset_focus.length; i++) {
              total_count += data_subset_focus[i].count;
            }

            // Update start and end dates and total count
            vis
              .select("#displayDates")
              .text(
                start_month == end_month
                  ? "in " + start_month
                  : "from " + start_month + " to " + end_month,
              );
            vis
              .select("#totalCount")
              .text(
                MetacatUI.appView.commaSeparateNumber(total_count) +
                  " " +
                  convert_metric_name(total_count),
              );

            // Fade all years in the bar chart not within the brush
            context.selectAll(".bar_context").style("opacity", function (d, i) {
              if (metricMonths_parsed.length == 1) {
                return "1";
              } else {
                return (d.month <= display_date_format.parse(end_month) &&
                  d.month >= left_date) ||
                  brush.empty()
                  ? "1"
                  : ".3";
              }
            });
          }

          function check_bounds() {
            // when brush stops moving:

            // check whether chart was scrolled out of bounds and fix,
            var b = brush.extent();
            var out_of_bounds = brush.extent().some(function (e) {
              return (e < x_full_extent[0]) | (e > x_full_extent[1]);
            });
            if (out_of_bounds) {
              b = move_in_bounds(b);
            }
          }

          function move_in_bounds(b) {
            // move back to boundaries if user pans outside min and max date.

            var year_in_ms = 31536000000,
              brush_start_new,
              brush_end_new;

            if (b[0] < x_full_extent[0]) {
              brush_start_new = x_full_extent[0];
            } else if (b[0] > x_full_extent[1]) {
              brush_start_new = x_full_extent[0];
            } else {
              brush_start_new = b[0];
            }

            if (b[1] > x_full_extent[1]) {
              brush_end_new = x_full_extent[1];
            } else if (b[1] < x_full_extent[0]) {
              brush_end_new = x_full_extent[1];
            } else {
              brush_end_new = b[1];
            }

            brush.extent([brush_start_new, brush_end_new]);

            brush(
              d3.select("#" + self.id + " > .context > .brush").transition(),
            );
            change_focus_brush();
            change_focus_zoom();

            return brush.extent();
          }

          function zoom_to_interval(d, i) {
            // action for buttons that zoom focus to certain time interval

            var b = brush.extent(),
              interval_ms,
              brush_end_new,
              brush_start_new;

            if (d == "year") {
              interval_ms = 31536000000;
            } else if (d == "month") {
              interval_ms = 2592000000;
            }

            if ((d == "year") | (d == "month")) {
              if (x_full_extent[1].getTime() - b[1].getTime() < interval_ms) {
                // if brush is too far to the right that increasing the right-hand brush boundary would make the chart go out of bounds....
                brush_start_new = new Date(
                  x_full_extent[1].getTime() - interval_ms,
                ); // ...then decrease the left-hand brush boundary...
                brush_end_new = x_full_extent[1]; //...and set the right-hand brush boundary to the maxiumum limit.
              } else {
                // otherwise, increase the right-hand brush boundary.
                brush_start_new = b[0];
                brush_end_new = new Date(b[0].getTime() + interval_ms);
              }
            } else if (d == "all") {
              brush_start_new = x_full_extent[0];
              brush_end_new = x_full_extent[1];
            } else {
              brush_start_new = b[0];
              brush_end_new = b[1];
            }

            brush.extent([brush_start_new, brush_end_new]);

            // now draw the brush to match our extent

            brush(
              d3.select("#" + self.id + " > .context > .brush").transition(),
            );
            // now fire the brushstart, brushmove, and check_bounds events
            brush.event(
              d3.select("#" + self.id + " > .context > .brush").transition(),
            );
          }

          // that's it!
        }

        //Re-render this view when the window is resized
        this.listenToWindowResize();

        return this;
      },

      /**
       * Adds a listener so when the window is resized, the chart is redrawn
       */
      listenToWindowResize: function () {
        if (!this.resizeCallback) {
          this.resizeCallback = this.render.bind(this);
          window.addEventListener("resize", this.resizeCallback, false);
        }
      },

      /**
       * Removes the window resize listener set in {@link MetricsChartView#listenToWindowResize}
       */
      stopListenToWindowResize: function () {
        //Remove the listener to window resize
        window.removeEventListener("resize", this.resizeCallback, false);
        delete this.resizeCallback;
      },

      /**
       * Cleans up listeners and other artifacts from this view
       */
      onClose: function () {
        this.stopListenToWindowResize();
      },
    },
  );

  return MetricsChartView;
});