|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
|
|
/****************************************** |
|
* Metric Modal Chart (Views, Downloads, Citations) |
|
********************************************/ |
|
|
|
/* When there is no data ...*/ |
|
|
|
#metric-modal .metric-chart text { |
|
fill: #565656; |
|
font-size: 9px; |
|
font-family: Helvetica, Arial, "sans serif"; |
|
} |
|
|
|
#metric-modal .metric-chart text.no-data { |
|
font-size: 16px; |
|
font-weight: 100; |
|
fill:#d0d0d0; |
|
} |
|
|
|
#metric-modal .metric-chart rect.no-data { |
|
fill: #f5f5f5; |
|
} |
|
|
|
/* When there is data ...*/ |
|
|
|
/* CB: padding to display better on bl.ocks.org */ |
|
#metric-modal { |
|
padding: 64px; |
|
} |
|
|
|
#metric-modal .metric-chart rect.plot-background{ |
|
fill: white; |
|
} |
|
|
|
/* complete data line */ |
|
#metric-modal .metric-chart path.line { |
|
fill: none; |
|
stroke: #00AA8D; /* default, changed in each theme */ |
|
stroke-width: 1.5px; |
|
clip-path: url(#clip); |
|
} |
|
|
|
/* complete data area */ |
|
#metric-modal .metric-chart path.area { |
|
fill: #00AA8D; /* CB default, changed in each theme */ |
|
opacity: 0.6; |
|
clip-path: url(#clip); |
|
cursor: move; /* fallback if grab cursor is unsupported */ |
|
cursor: grab; |
|
} |
|
|
|
/*missing data line */ |
|
#metric-modal .metric-chart path.line_missing { |
|
fill: none; |
|
stroke: #00AA8D; /* default, changed in each theme */ |
|
stroke-width: 1.5px; |
|
stroke-dasharray: 1.5px 2px; |
|
clip-path: url(#clip); |
|
} |
|
/*missing data area */ |
|
#metric-modal .metric-chart path.area_missing { |
|
fill: #00AA8D; /* CB default, changed in each theme */ |
|
opacity: 0.15; |
|
clip-path: url(#clip); |
|
cursor: move; /* fallback if grab cursor is unsupported */ |
|
cursor: grab; |
|
} |
|
|
|
#metric-modal .metric-chart path.area_missing:active, |
|
#metric-modal .metric-chart path.area:active { |
|
cursor: move; /* fallback if grab cursor is unsupported */ |
|
cursor: grabbing; |
|
} |
|
|
|
#metric-modal .metric-chart .axis { |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
#metric-modal .metric-chart .x.axis .domain{ |
|
display:none; |
|
} |
|
|
|
#metric-modal .metric-chart .x.axis line { |
|
stroke: white; |
|
opacity: 0.4; |
|
} |
|
|
|
#metric-modal .metric-chart .context .x.axis line { |
|
display: none; |
|
} |
|
|
|
#metric-modal .metric-chart .y.axis .domain{ |
|
display: none; |
|
} |
|
|
|
#metric-modal .metric-chart .y.axis.title{ |
|
font-size: 13px; |
|
font-weight: 100; |
|
} |
|
|
|
#metric-modal .metric-chart .y.axis line { |
|
stroke: #565656; |
|
stroke-dasharray: 2,2; |
|
stroke-opacity: 0.3; |
|
} |
|
|
|
#metric-modal .metric-chart .brush .extent { |
|
fill-opacity: .07; |
|
shape-rendering: crispEdges; |
|
clip-path: url(#clip); |
|
} |
|
|
|
#metric-modal .metric-chart rect.pane { |
|
cursor: move; /* fallback if grab cursor is unsupported */ |
|
cursor: grab; |
|
fill: white; |
|
pointer-events: all; |
|
} |
|
|
|
#metric-modal .metric-chart rect.pane:active { |
|
cursor: move; /* fallback if grab cursor is unsupported */ |
|
cursor: grabbing; |
|
} |
|
|
|
/* brush handles */ |
|
#metric-modal .metric-chart .resize .handle { |
|
fill: #555; |
|
} |
|
|
|
#metric-modal .metric-chart .resize .handle-mini { |
|
fill: white; |
|
stroke-width: 1px; |
|
stroke: #555; |
|
} |
|
|
|
#metric-modal .metric-chart .scale_button { |
|
cursor: pointer; |
|
} |
|
|
|
#metric-modal .metric-chart .scale_button rect { |
|
fill: #eaeaea; |
|
} |
|
#metric-modal .metric-chart .scale_button:hover text { |
|
fill: #417DD6; |
|
transition: all 0.1s cubic-bezier(.25,.8,.25,1); |
|
} |
|
|
|
#metric-modal .metric-chart text#displayDates { |
|
font-weight: bold; |
|
} |
|
|
|
/* circle style */ |
|
#metric-modal .metric-chart .dot { |
|
fill: white; |
|
stroke: #00AA8D; /* default, changed in each theme */ |
|
stroke-width: 1.5px; |
|
clip-path: url(#clip); |
|
cursor: default; |
|
transition: stroke-width 0.06s ease-in; |
|
transition: stroke 0.06s ease-in; |
|
} |
|
|
|
#metric-modal .metric-chart .dot:hover { |
|
stroke: #00997e; |
|
stroke-width: 6px; |
|
transition: stroke-width 0.06s ease-out; |
|
transition: stroke 0.06s ease-out; |
|
} |
|
|
|
div.tooltip { |
|
position: fixed; |
|
text-align: center; |
|
width: 95px; |
|
height: 25px; |
|
padding: 2px; |
|
font: 10px sans-serif; |
|
background: white; |
|
color: #565656; |
|
border: 0px; |
|
border-radius: 2px; |
|
pointer-events: none; |
|
box-shadow: 0 0px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); |
|
|
|
} |
|
|
|
</style> |
|
<body> |
|
<div id="metric-modal"></div> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script> |
|
|
|
// example data |
|
var metricName = "views"; |
|
var metricCount = [1, 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, 8, 2, 1, 4, 2, 4, 1, 3, 1, 2, 1, 1, 3, 1, 1, 5, 1, 1, 4]; |
|
var 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"]; |
|
var optwidth = 600; |
|
var optheight = 370; |
|
|
|
|
|
/* |
|
* ======================================================================== |
|
* Prepare data |
|
* ======================================================================== |
|
*/ |
|
|
|
|
|
// to find missing data: get a sequence of all months between the min and max dates in the data. |
|
|
|
// change dates to milliseconds |
|
metricMonths.forEach(function(part, index, theArray) { |
|
theArray[index] = d3.time.format("%Y-%m").parse(part).getTime(); |
|
}); |
|
|
|
// get a list of all months in the range of data |
|
var monthArray = d3.time.scale() |
|
.domain(d3.extent(metricMonths)) |
|
.ticks(d3.time.months, 1); |
|
|
|
// check if there is data for each month in monthArray, if so append count, otherwise append null |
|
var dataset = []; |
|
var running_total = 0; |
|
for(var i=0; i<monthArray.length; i++){ |
|
var n = metricMonths.indexOf(monthArray[i].getTime()); |
|
if (n>-1) { |
|
var running_total = running_total + metricCount[n]; |
|
var data_point = true; |
|
var new_count = metricCount[n]; |
|
} else { |
|
var running_total = running_total; |
|
var data_point = false; |
|
var new_count = null; |
|
} |
|
dataset.push({count: running_total, month: monthArray[i], data_point: data_point, new_count: new_count}); |
|
} |
|
|
|
// a dataset without the null values is also needed to draw the missing data lines/areas |
|
var dataset_no_null = dataset.filter(function(d) { return d.data_point !== false; }); |
|
|
|
/* |
|
* ======================================================================== |
|
* sizing |
|
* ======================================================================== |
|
*/ |
|
|
|
/* === Focus chart === */ |
|
|
|
var margin = {top: 20, right: 30, bottom: 100, left: 20}, |
|
width = optwidth - margin.left - margin.right, |
|
height = optheight - margin.top - margin.bottom; |
|
|
|
/* === Context chart === */ |
|
|
|
var margin_context = {top: 320, right: 30, bottom: 20, left: 20}, |
|
height_context = optheight - margin_context.top - margin_context.bottom; |
|
|
|
/* |
|
* ======================================================================== |
|
* x and y coordinates |
|
* ======================================================================== |
|
*/ |
|
|
|
// the date range of available data: |
|
var dataXrange = d3.extent(dataset, function(d) { return d.month; }); |
|
var dataYrange = [0, d3.max(dataset, function(d) { return d.count; })]; |
|
|
|
// maximum date range allowed to display |
|
var mindate = dataXrange[0], // use the range of the data |
|
maxdate = dataXrange[1]; |
|
|
|
var DateFormat = d3.time.format("%b %Y"); |
|
|
|
var dynamicDateFormat = timeFormat([ |
|
[d3.time.format("%Y"), function() { return true; }],// <-- how to display when Jan 1 YYYY |
|
[d3.time.format("%b %Y"), function(d) { return d.getMonth(); }], |
|
[function(){return "";}, function(d) { return d.getDate() != 1; }] |
|
]); |
|
|
|
/* === Focus Chart === */ |
|
|
|
var x = d3.time.scale() |
|
.range([0, (width)]) |
|
.domain(dataXrange); |
|
|
|
var y = d3.scale.linear() |
|
.range([height, 0]) |
|
.domain(dataYrange); |
|
|
|
var xAxis = d3.svg.axis() |
|
.scale(x) |
|
.orient("bottom") |
|
.tickSize(-(height)) |
|
.ticks(customTickFunction) |
|
.tickFormat(dynamicDateFormat); |
|
|
|
var yAxis = d3.svg.axis() |
|
.scale(y) |
|
.ticks(4) |
|
.tickSize(-(width)) |
|
.orient("right"); |
|
|
|
/* === Context Chart === */ |
|
|
|
var x2 = d3.time.scale() |
|
.range([0, width]) |
|
.domain([mindate, maxdate]); |
|
|
|
var y2 = d3.scale.linear() |
|
.range([height_context, 0]) |
|
.domain(y.domain()); |
|
|
|
var xAxis_context = d3.svg.axis() |
|
.scale(x2) |
|
.orient("bottom") |
|
.ticks(customTickFunction) |
|
.tickFormat(dynamicDateFormat); |
|
|
|
/* |
|
* ======================================================================== |
|
* Plotted line and area variables |
|
* ======================================================================== |
|
*/ |
|
|
|
/* === Focus Chart === */ |
|
|
|
var line = d3.svg.line() |
|
.defined(function(d) { return d.data_point == true; }) |
|
.x(function(d) { return x(d.month); }) |
|
.y(function(d) { return y(d.count); }); |
|
|
|
var area = d3.svg.area() |
|
.defined(line.defined()) |
|
.x(function(d) { return x(d.month); }) |
|
.y0((height)) |
|
.y1(function(d) { return y(d.count); }); |
|
|
|
var line_missing = d3.svg.line() |
|
.x(function(d) { return x(d.month); }) |
|
.y(function(d) { return y(d.count); }); |
|
|
|
var area_missing = d3.svg.area() |
|
.x(function(d) { return x(d.month); }) |
|
.y0((height)) |
|
.y1(function(d) { return y(d.count); }); |
|
|
|
|
|
/* === Context Chart === */ |
|
|
|
var line_context = d3.svg.line() |
|
.defined(function(d) { return d.data_point == true; }) |
|
.x(function(d) { return x2(d.month); }) |
|
.y(function(d) { return y2(d.count); }); |
|
|
|
var area_context = d3.svg.area() |
|
.defined(line_context.defined()) |
|
.x(function(d) { return x2(d.month); }) |
|
.y0((height_context)) |
|
.y1(function(d) { return y2(d.count); }); |
|
|
|
var line_context_missing = d3.svg.line() |
|
.x(function(d) { return x2(d.month); }) |
|
.y(function(d) { return y2(d.count); }); |
|
|
|
var area_context_missing = d3.svg.area() |
|
.x(function(d) { return x2(d.month); }) |
|
.y0((height_context)) |
|
.y1(function(d) { return y2(d.count); }); |
|
|
|
/* |
|
* ======================================================================== |
|
* Variables for brushing and zooming behaviour |
|
* ======================================================================== |
|
*/ |
|
|
|
var brush = d3.svg.brush() |
|
.x(x2) |
|
.on("brush", brushed) |
|
.on("brushend", brushend); |
|
|
|
var zoom = d3.behavior.zoom() |
|
.on("zoom", draw) |
|
.on("zoomend", brushend); |
|
|
|
/* |
|
* ======================================================================== |
|
* Define the SVG area ("vis") and append all the layers |
|
* ======================================================================== |
|
*/ |
|
|
|
// === the main components === // |
|
|
|
var vis = d3.select("#metric-modal").append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.attr("class", "metric-chart");// CB -- "line-chart" -- CB // |
|
|
|
vis.append("defs").append("clipPath") |
|
.attr("id", "clip") |
|
.append("rect") |
|
.attr("width", width) |
|
.attr("height", height); |
|
// clipPath is used to keep line and area from moving outside of plot area when user zooms/scrolls/brushes |
|
|
|
var rect = vis.append("svg: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 + ")"); |
|
|
|
rect.call(zoom) |
|
.call(draw); |
|
|
|
// === current date range text & zoom buttons === // |
|
|
|
var display_range_group = vis.append("g") |
|
.attr("id", "buttons_group") |
|
.attr("transform", "translate(" + 0 + ","+ 0 +")"); |
|
|
|
var expl_text = display_range_group.append("text") |
|
.text("Showing data from: ") |
|
.style("text-anchor", "start") |
|
.attr("transform", "translate(" + 0 + ","+ 10 +")"); |
|
|
|
display_range_group.append("text") |
|
.attr("id", "displayDates") |
|
.text(DateFormat(dataXrange[0]) + " - " + DateFormat(dataXrange[1])) |
|
.style("text-anchor", "start") |
|
.attr("transform", "translate(" + 82 + ","+ 10 +")"); |
|
|
|
var expl_text = display_range_group.append("text") |
|
.text("Zoom to: ") |
|
.style("text-anchor", "start") |
|
.attr("transform", "translate(" + 180 + ","+ 10 +")"); |
|
|
|
// === the zooming/scaling buttons === // |
|
|
|
var button_width = 40; |
|
var button_height = 14; |
|
|
|
// don't show year button if < 1 year of data |
|
var dateRange = dataXrange[1] - dataXrange[0], |
|
ms_in_year = 31540000000; |
|
|
|
if (dateRange < ms_in_year) { |
|
var button_data =["month","data"]; |
|
} else { |
|
var button_data =["year","month","data"]; |
|
}; |
|
|
|
var button = display_range_group.selectAll("g") |
|
.data(button_data) |
|
.enter().append("g") |
|
.attr("class", "scale_button") |
|
.attr("transform", function(d, i) { return "translate(" + (220 + i*button_width + i*10) + ",0)"; }) |
|
.on("click", scaleDate); |
|
|
|
button.append("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(yAxis) |
|
.attr("transform", "translate(" + (width) + ", 0)"); |
|
|
|
// missing data area |
|
focus.append("path") |
|
.datum(dataset) |
|
.attr("class", "area_missing") |
|
.attr("d", area_missing) |
|
.call(zoom) |
|
.call(draw); |
|
|
|
// complete data area |
|
focus.append("path") |
|
.datum(dataset) |
|
.attr("class", "area") |
|
.attr("d", area) |
|
.call(zoom) |
|
.call(draw); |
|
|
|
// x-axis |
|
focus.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(xAxis); |
|
|
|
// missing data line |
|
focus.append("path") |
|
.datum(dataset) |
|
.attr("class", "line_missing") |
|
.attr("d", line_missing); |
|
|
|
// complete data line |
|
focus.append("path") |
|
.datum(dataset) |
|
.attr("class", "line") |
|
.attr("d", line); |
|
|
|
// circles |
|
focus.selectAll(".dot") |
|
.data(dataset_no_null) |
|
//.defined(function(d) { return d.data_point == true; }) |
|
.enter().append("circle") |
|
.attr("class", "dot") |
|
.attr("r", 3) |
|
.attr("cx", function(d) { return x(d.month); }) |
|
.attr("cy", function(d) { return y(d.count); }) |
|
.on("mouseover", function(d) {show_tooltip(d)} ) |
|
.on("mouseout", function(d) {hide_tooltip(d)} ); |
|
|
|
/* === tooltip === */ |
|
var div = d3.select("#metric-modal").append("div") |
|
.attr("class", "tooltip") |
|
.style("opacity", 0); |
|
|
|
/* === context chart === */ |
|
|
|
context.append("path") |
|
.datum(dataset) |
|
.attr("class", "area_missing") |
|
.attr("d", area_context_missing); |
|
|
|
context.append("path") |
|
.datum(dataset) |
|
.attr("class", "area") |
|
.attr("d", area_context); |
|
|
|
context.append("path") |
|
.datum(dataset) |
|
.attr("class", "line_missing") |
|
.attr("d", line_context_missing); |
|
|
|
context.append("path") |
|
.datum(dataset) |
|
.attr("class", "line") |
|
.attr("d", line_context); |
|
|
|
context.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + height_context + ")") |
|
.call(xAxis_context); |
|
|
|
/* === brush (part of context chart) === */ |
|
|
|
var brushg = context.append("g") |
|
.attr("class", "x brush") |
|
.call(brush); |
|
|
|
brushg.selectAll(".extent") |
|
.attr("y", -6) |
|
.attr("height", height_context + 8); |
|
// .extent is the actual window/rectangle showing what's in focus |
|
|
|
brushg.selectAll(".resize") |
|
.append("rect") |
|
.attr("class", "handle") |
|
.attr("transform", "translate(0," + -3 + ")") |
|
.attr('rx', 2) |
|
.attr('ry', 2) |
|
.attr("height", height_context + 6) |
|
.attr("width", 3); |
|
|
|
brushg.selectAll(".resize") |
|
.append("rect") |
|
.attr("class", "handle-mini") |
|
.attr("transform", "translate(-2,8)") |
|
.attr('rx', 3) |
|
.attr('ry', 3) |
|
.attr("height", (height_context/2)) |
|
.attr("width", 7); |
|
// .resize are the handles on either size |
|
// of the 'window' (each is made of a set of rectangles) |
|
|
|
/* === y axis title === */ |
|
|
|
vis.append("text") |
|
.attr("class", "y axis title") |
|
.text("Monthly " + this.metricName) |
|
.attr("x", (-(height/2))) |
|
.attr("y", 0) |
|
.attr("dy", "1em") |
|
.attr("transform", "rotate(-90)") |
|
.style("text-anchor", "middle"); |
|
|
|
// allows zooming before any brush action |
|
zoom.x(x); |
|
|
|
/* |
|
* ======================================================================== |
|
* Functions |
|
* ======================================================================== |
|
*/ |
|
|
|
// === tick/date formatting functions === |
|
// from: https://stackoverflow.com/questions/20010864/d3-axis-labels-become-too-fine-grained-when-zoomed-in |
|
|
|
function timeFormat(formats) { |
|
return function(date) { |
|
var i = formats.length - 1, f = formats[i]; |
|
while (!f[1](date)) f = formats[--i]; |
|
return f[0](date); |
|
}; |
|
}; |
|
|
|
function customTickFunction(t0, t1, dt) { |
|
var labelSize = 42; // |
|
var maxTotalLabels = Math.floor(width / labelSize); |
|
|
|
function step(date, offset) |
|
{ |
|
date.setMonth(date.getMonth() + offset); |
|
} |
|
|
|
var time = d3.time.month.ceil(t0), 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 > maxTotalLabels ; i++) |
|
times = _.filter(timesCopy, function(d){ |
|
return (d.getMonth()) % monthFactors[i] == 0; |
|
}); |
|
|
|
return times; |
|
}; |
|
|
|
// === tooltip functions === // |
|
|
|
// from: http://bl.ocks.org/d3noob/a22c42db65eb00d4e369 |
|
function show_tooltip(d) { |
|
|
|
if (d.new_count == 1) { |
|
var metricName_point = metricName.slice(0, -1); |
|
} else { |
|
var metricName_point = metricName; |
|
} |
|
|
|
div.transition() |
|
.duration(60) |
|
.style("opacity", 0.98); |
|
div.html(d.new_count + " " + metricName_point + " in <b>" + DateFormat(d.month) + "</b><br/>" + d.count + " " + metricName + " total") |
|
.style("left", (d3.event.pageX -45) + "px") |
|
.style("top", (d3.event.pageY -50) + "px"); |
|
}; |
|
|
|
function hide_tooltip(d) { |
|
div.transition() |
|
.duration(60) |
|
.style("opacity", 0); |
|
}; |
|
|
|
|
|
// === brush and zoom functions === |
|
|
|
function brushed() { |
|
x.domain(brush.empty() ? x2.domain() : brush.extent()); |
|
common_behaviour(); |
|
// Reset zoom scale's domain |
|
zoom.x(x); |
|
updateDisplayDates(); |
|
setYdomain(); |
|
|
|
} |
|
|
|
function draw() { |
|
setYdomain(); |
|
common_behaviour(); |
|
// Force changing brush range |
|
brush.extent(x.domain()); |
|
vis.select(".brush").call(brush); |
|
// and update the text showing range of dates. |
|
updateDisplayDates(); |
|
} |
|
|
|
function common_behaviour() { |
|
focus.select(".area").attr("d", area); |
|
focus.select(".line").attr("d", line); |
|
focus.select(".area_missing").attr("d", area_missing); |
|
focus.select(".line_missing").attr("d", line_missing); |
|
focus.select(".x.axis").call(xAxis); |
|
focus.selectAll(".dot") |
|
.attr("cx", function(d) { return x(d.month); }) |
|
.attr("cy", function(d) { return y(d.count); }); |
|
}; |
|
|
|
function brushend() { |
|
// 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 < mindate | e > maxdate; }); |
|
if (out_of_bounds){ b = moveInBounds(b) }; |
|
|
|
}; |
|
|
|
function updateDisplayDates() { |
|
|
|
var b = brush.extent(); |
|
// update the text that shows the range of displayed dates |
|
var localBrushDateStart = (brush.empty()) ? DateFormat(dataXrange[0]) : DateFormat(b[0]), |
|
localBrushDateEnd = (brush.empty()) ? DateFormat(dataXrange[1]) : DateFormat(b[1]); |
|
|
|
// Update start and end dates in upper right-hand corner |
|
d3.select("#displayDates") |
|
.text(localBrushDateStart == localBrushDateEnd ? localBrushDateStart : localBrushDateStart + " - " + localBrushDateEnd); |
|
}; |
|
|
|
function moveInBounds(b) { |
|
// move back to boundaries if user pans outside min and max date. |
|
|
|
var ms_in_year = 31536000000, |
|
brush_start_new, |
|
brush_end_new; |
|
|
|
if (b[0] < mindate) { brush_start_new = mindate; } |
|
else if (b[0] > maxdate) { brush_start_new = new Date(maxdate.getTime() - ms_in_year); } |
|
else { brush_start_new = b[0]; }; |
|
|
|
if (b[1] > maxdate) { brush_end_new = maxdate; } |
|
else if (b[1] < mindate) { brush_end_new = new Date(mindate.getTime() + ms_in_year); } |
|
else { brush_end_new = b[1]; }; |
|
|
|
brush.extent([brush_start_new, brush_end_new]); |
|
|
|
brush(d3.select(".brush").transition()); |
|
brushed(); |
|
draw(); |
|
|
|
return(brush.extent()) |
|
}; |
|
|
|
function setYdomain(){ |
|
// this function dynamically changes the y-axis to fit the data in focus |
|
|
|
// get the min and max date in focus |
|
var xleft = new Date(x.domain()[0]); |
|
var xright = new Date(x.domain()[1]); |
|
|
|
// a function that finds the nearest point to the right of a point |
|
var bisectDate = d3.bisector(function(d) { return d.month; }).right; |
|
|
|
// get the y value of the line at the left edge of view port: |
|
var iL = bisectDate(dataset, xleft); |
|
|
|
if (dataset[iL] !== undefined && dataset[iL-1] !== undefined) { |
|
|
|
var left_dateBefore = dataset[iL-1].month, |
|
left_dateAfter = dataset[iL].month; |
|
|
|
var intfun = d3.interpolateNumber(dataset[iL-1].count, dataset[iL].count); |
|
var yleft = intfun((xleft-left_dateBefore)/(left_dateAfter-left_dateBefore)); |
|
} else { |
|
var yleft = 0; |
|
} |
|
|
|
// get the x value of the line at the right edge of view port: |
|
var iR = bisectDate(dataset, xright); |
|
|
|
if (dataset[iR] !== undefined && dataset[iR-1] !== undefined) { |
|
|
|
var right_dateBefore = dataset[iR-1].month, |
|
right_dateAfter = dataset[iR].month; |
|
|
|
var intfun = d3.interpolateNumber(dataset[iR-1].count, dataset[iR].count); |
|
var yright = intfun((xright-right_dateBefore)/(right_dateAfter-right_dateBefore)); |
|
} else { |
|
var yright = 0; |
|
} |
|
|
|
// get the y values of all the actual data points that are in view |
|
var dataSubset = dataset.filter(function(d){ return d.month >= xleft && d.month <= xright; }); |
|
var countSubset = []; |
|
dataSubset.map(function(d) {countSubset.push(d.count);}); |
|
|
|
// add the edge values of the line to the array of counts in view, get the max y; |
|
countSubset.push(yleft); |
|
countSubset.push(yright); |
|
var ymax_new = d3.max(countSubset); |
|
|
|
if(ymax_new == 0){ |
|
ymax_new = dataYrange[1]; |
|
} |
|
|
|
// reset and redraw the yaxis |
|
y.domain([0, ymax_new*1.05]); |
|
focus.select(".y.axis").call(yAxis); |
|
|
|
}; |
|
|
|
function scaleDate(d,i) { |
|
// action for buttons that scale 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((maxdate.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(maxdate.getTime() - interval_ms); // ...then decrease the left-hand brush boundary... |
|
brush_end_new = maxdate; //...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 == "data") { |
|
brush_start_new = dataXrange[0]; |
|
brush_end_new = dataXrange[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 brushend events |
|
brush.event(d3.select(".brush").transition()); |
|
}; |
|
|
|
</script> |
|
</body> |