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;
});