/*global define */ define(['jquery', 'underscore', 'backbone', 'd3'], function($, _, Backbone, d3) { 'use strict'; /* * The MetricsChartView will render an SVG times-series chart using D3 that shows the number of metrics over time. */ var MetricsChartView = Backbone.View.extend({ 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"; // !!!!!!!!!!!!!!!!!!!!!! DUMMY DATA FOR TESTING !!!!!!!!!!!!!!!!!!!!!! // this.metricCount = [0, 3, 1, 2, 1, 1, 1, 1, 2, 2, 3, 1, 2, 1, 4, 3, 2, 1, 1, 1, 1, 1, 4, 2, 1, 2, 1, 2, 1, 4, 2, 4, 7, 3, 1, 2, 1, 1, 3, 1, 1, 5, 1, 1, 4]; // this.metricMonths = ["2018-06", "2013-04", "2015-11", "2012-10", "2014-09", "2014-02", "2016-02", "2016-04", "2016-06", "2014-12", "2013-07", "2017-01", "2015-10", "2012-12", "2013-05", "2018-04", "2015-06", "2017-03", "2014-08", // "2017-07", "2013-02", "2012-07", "2016-03", "2017-06", "2018-07", "2014-10", "2013-01", "2013-10", "2017-11", "2014-05", "2012-11", "2015-01", "2018-03", "2015-12", "2015-08", "2016-08", "2014-11", "2014-01", // "2013-06", "2012-08", "2015-09", "2016-07", "2013-03", "2012-09", "2016-05"]; //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! this.id = options.id || "metrics-chart"; 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", render: function(){ /* * ======================================================================== * 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] } // 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." var margin = {top: 25, right: 40, bottom: 40, left: 40}, width = this.width - margin.left - margin.right, height = this.height - margin.top - margin.bottom; 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}, width = this.width - margin.left - margin.right, height = this.height - margin.top - margin.bottom; // 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) { metricMonths_parsed[index] = input_date_format.parse(part).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(){ // add behaviours pane.call(zoom) .call(change_focus_zoom); 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"); // tooltip div must be created after timeout so that parent div // has time to load (otherwise can't select .metric-chart div) d3.select(".metric-chart") .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) { 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); }; function hide_tooltip(d) { d3.select(".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(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(".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(".brush").transition()); // now fire the brushstart, brushmove, and check_bounds events brush.event(d3.select(".brush").transition()); }; // that's it! } return this; } }); return MetricsChartView; });