app/scripts/customgraph/customgraph.controller.js
(function() {
'use strict';
/*
source: D3 example @ http://bl.ocks.org/rkirsling/33a9e350516da54a5d4f
*/
function CustomGraphController($scope, $window, $timeout, $http, d3Service, NcwmsService, Messagebus, UserAgent) {
this.mobile = UserAgent.mobile;
this.selectedLabel = '';
this.selectedUnits = '';
this.selectedPalette = 'default';
this.boundingRect = null;
this.selectedDataset = null;
this.globalSelectedDataset = null;
this.followglobalSelectedDataset = true;
this.activated = $scope.activated;
this.logarithmic = false;
this.errorMessage = 'test 123';
this.setSelections = function() {
if (this.followglobalSelectedDataset) {
if (this.globalSelectedDataset === null) {
this.globalSelectedDataset = NcwmsService.datasets[0];
}
this.selectedLabel = this.globalSelectedDataset.label;
this.selectedUnits = this.globalSelectedDataset.units;
if (this.boundingRect !== null) {
this.errorMessage = '';
NcwmsService.getFeatureInfoSeries(this.globalSelectedDataset, this.selectedPalette, this.boundingRect, this.getFeatureInfoSeriesCallbackSuccess, this.getFeatureInfoSeriesCallbackFailure);
} else {
this.errorMessage = 'please click a point with data';
}
} else {
if (this.selectedDataset !== null) {
this.selectedLabel = this.selectedDataset.label;
this.selectedUnits = this.selectedDataset.units;
NcwmsService.getFeatureInfoSeries(this.selectedDataset, this.selectedPalette, this.boundingRect, this.getFeatureInfoSeriesCallbackSuccess, this.getFeatureInfoSeriesCallbackFailure);
}
}
};
this.toggleActivated = function() {
this.setSelections();
this.activated = !this.activated;
};
this.toggleLogarithmic = function() {
this.setSelections();
};
this.getDatasets = function() {
return NcwmsService.datasets;
};
this.setSubscriptions = function() {
Messagebus.subscribe('ncwmsPaletteSelected', function(event, value) {
if (this.selectedPalette !== value) {
this.selectedPalette = value;
}
}.bind(this));
Messagebus.subscribe('cesiumCoordinatesClicked', function(event, value) {
this.boundingRect = {
'leftTopLon': value.leftTopLon,
'leftTopLat': value.leftTopLat,
'rightBottomLon': value.rightBottomLon,
'rightBottomLat': value.rightBottomLat
};
this.setSelections();
}.bind(this));
Messagebus.subscribe('ncwmsDatasetSelected', function(event, value) {
if (this.globalSelectedDataset !== value) {
this.globalSelectedDataset = value;
this.setSelections();
}
}.bind(this));
}.bind(this);
this.selectDataset = function(dataset) {
this.selectedDataset = dataset;
this.followglobalSelectedDataset = false;
this.setSelections();
}.bind(this);
this.selectDefaultDataset = function() {
this.followglobalSelectedDataset = true;
this.setSelections();
}.bind(this);
this.init = function(element, attrs) {
this.dragging = false;
var container = element.children[0].children[1];
var hoverContainer, hoverLine, hoverLineXOffset, hoverLineYOffset, hoverLineGroup,
timeIndicatorLine, timeIndicatorLineXOffset, timeIndicatorLineYOffset, timeIndicatorLineGroup;
var x, y;
// used to track if the user is interacting via mouse/finger instead of trying to determine
// by analyzing various element class names to see if they are visible or not
var userCurrentlyInteracting = false;
var currentUserPositionX = -1;
d3Service.d3().then(function(d3) {
var renderTimeout;
var margin = parseInt(attrs.margin) || 0;
// Browser onresize event
window.onresize = function() {
$scope.$apply();
};
// Watch for resize event
$scope.$watch(function() {
return angular.element($window)[0].innerWidth;
}, function() {
this.render(this.data);
}.bind(this));
var parseISO = d3.time.format.utc('%Y-%m-%dT%H:%M:%S.%L%Z').parse;
this.getFeatureInfoSeriesCallbackSuccess = function(graphInfo) {
this.data = graphInfo.map(function(d) {
return {
date: parseISO(d.time),
pct05: d.value - d.error - d.error,
pct25: d.value - d.error,
pct50: d.value,
pct75: d.value + d.error,
pct95: d.value + d.error + d.error
};
}.bind(this));
this.render(this.data);
}.bind(this);
this.getFeatureInfoSeriesCallbackFailure = function(errorMessage) {
console.log(errorMessage);
this.render();
}.bind(this);
this.setSubscriptions();
this.render = function(data) {
// remove all previous items before render
d3.select(container).selectAll('*').remove();
if (!this.activated) {
return;
}
// If we don't pass any data, return out of the element
if (!data) {
this.errorMessage = 'please click a point with data';
return;
} else {
this.errorMessage = '';
}
if (renderTimeout) {
clearTimeout(renderTimeout);
}
renderTimeout = $timeout(function() {
// setup variables
var width = d3.select(element)[0][0].children[0].children[1].offsetWidth - margin;
this.addAxesAndLegend = function(svg, xAxis, yAxis, margin, chartWidth, chartHeight) {
var legendWidth = 0,
legendHeight = 0;
svg.append('clipPath')
.attr('id', 'axes-clip')
.append('polygon')
.attr('points', (-margin.left) + ',' + (-margin.top) + ' ' +
(chartWidth - legendWidth - 1) + ',' + (-margin.top) + ' ' +
(chartWidth - legendWidth - 1) + ',' + legendHeight + ' ' +
(chartWidth + margin.right) + ',' + legendHeight + ' ' +
(chartWidth + margin.right) + ',' + (chartHeight + margin.bottom) + ' ' +
(-margin.left) + ',' + (chartHeight + margin.bottom));
var axes = svg.append('g')
.attr('clip-path', 'url(#axes-clip)');
axes.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + chartHeight + ')')
.call(xAxis);
axes.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end')
.text(this.selectedUnits);
};
this.drawPaths = function(svg, data, x, y, chartWidth, chartHeight) {
var pathContainer = svg.append('g');
var upperOuterArea = d3.svg.area()
.interpolate('basis')
.x(function(d) {
return x(d.date) || 1;
}).y0(function(d) {
return y(d.pct95);
}).y1(function(d) {
return y(d.pct75);
});
var upperInnerArea = d3.svg.area()
.interpolate('basis')
.x(function(d) {
return x(d.date) || 1;
})
.y0(function(d) {
return y(d.pct75);
})
.y1(function(d) {
return y(d.pct50);
});
var medianLine = d3.svg.line()
.interpolate('basis')
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.pct50);
});
var lowerInnerArea = d3.svg.area()
.interpolate('basis')
.x(function(d) {
return x(d.date) || 1;
})
.y0(function(d) {
return y(d.pct50);
})
.y1(function(d) {
return y(d.pct25);
});
var lowerOuterArea = d3.svg.area()
.interpolate('basis')
.x(function(d) {
return x(d.date) || 1;
})
.y0(function(d) {
return y(d.pct25);
})
.y1(function(d) {
return y(d.pct05);
});
pathContainer.datum(data);
pathContainer.append('path')
.attr('class', 'area upper outer')
.attr('d', upperOuterArea)
.attr('clip-path', 'url(#rect-clip)');
pathContainer.append('path')
.attr('class', 'area lower outer')
.attr('d', lowerOuterArea)
.attr('clip-path', 'url(#rect-clip)');
pathContainer.append('path')
.attr('class', 'area upper inner')
.attr('d', upperInnerArea)
.attr('clip-path', 'url(#rect-clip)');
pathContainer.append('path')
.attr('class', 'area lower inner')
.attr('d', lowerInnerArea)
.attr('clip-path', 'url(#rect-clip)');
pathContainer.append('path')
.attr('class', 'median-line')
.attr('d', medianLine)
.attr('clip-path', 'url(#rect-clip)');
hoverContainer = container.querySelector('g .lines');
// add a 'hover' line that we'll show as a user moves their mouse (or finger)
// so we can use it to show detailed values of each line
hoverLineGroup = pathContainer.append('g')
.attr('class', 'hover-line');
// add the line to the group
hoverLine = hoverLineGroup
.append('line')
.attr('x1', 10).attr('x2', 10) // vertical line so same value on each
.attr('y1', 0).attr('y2', chartHeight); // top to bottom
// hide it by default
hoverLine.classed('hide', true);
// add a 'time indicator' line that we'll show as time progresses
timeIndicatorLineGroup = pathContainer.append('g')
.attr('class', 'time-indicator-line');
// add the line to the group
timeIndicatorLine = timeIndicatorLineGroup
.append('line')
.attr('x1', 10).attr('x2', 10) // vertical line so same value on each
.attr('y1', 0).attr('y2', chartHeight); // top to bottom
};
this.startTransitions = function(svg, chartWidth, chartHeight, rectClip) {
rectClip.transition()
.duration(1000)
.attr('width', chartWidth);
};
this.handleMouseClick = function(event, chartWidth, xAxis) {
var mouseX = event.pageX - hoverLineXOffset;
var value = xAxis.invert(mouseX);
Messagebus.publish('d3TimeSelected', value);
};
/**
* Called when a user mouses over the graph.
*/
this.handleMouseOverGraph = function(event, chartWidth, chartHeight, xAxis) {
var mouseX = event.pageX - hoverLineXOffset;
var mouseY = event.pageY - hoverLineYOffset;
//debug('MouseOver graph [' + containerId + '] => x: ' + mouseX + ' y: ' + mouseY + ' height: ' + h + ' event.clientY: ' + event.clientY + ' offsetY: ' + event.offsetY + ' pageY: ' + event.pageY + ' hoverLineYOffset: ' + hoverLineYOffset)
if (mouseX >= 0 && mouseX <= chartWidth && mouseY >= 0 && mouseY <= chartHeight) {
if (this.dragging) {
// show the hover line
hoverLine.classed('hide', false);
// set position of hoverLine
hoverLine.attr('x1', mouseX).attr('x2', mouseX);
var value = xAxis.invert(mouseX);
Messagebus.publish('d3TimeSelected', value);
// user is interacting
userCurrentlyInteracting = true;
currentUserPositionX = mouseX;
} else {
// show the hover line
hoverLine.classed('hide', false);
// set position of hoverLine
hoverLine.attr('x1', mouseX).attr('x2', mouseX);
// user is interacting
userCurrentlyInteracting = true;
currentUserPositionX = mouseX;
}
} else {
// proactively act as if we've left the area since we're out of the bounds we want
this.handleMouseOutGraph(event);
}
};
/**
* Called when a user mouses over the graph.
*/
this.handleTimeChange = function(value) {
// set position of timeIndicatorLine
timeIndicatorLine.attr('x1', x(value)).attr('x2', x(value));
};
this.handleMouseOutGraph = function() {
// hide the hover-line
hoverLine.classed('hide', true);
// user is no longer interacting
userCurrentlyInteracting = false;
currentUserPositionX = -1;
};
this.makeChart = function(data) {
var svgWidth = width;
var svgHeight = width / 2;
var margin = {
top: 20,
right: 20,
bottom: 40,
left: 60
};
var chartWidth = svgWidth - margin.left - margin.right;
var chartHeight = svgHeight - margin.top - margin.bottom;
x = d3.time.scale()
.range([0, chartWidth])
.domain(d3.extent(data, function(d) {
return d.date;
}));
if (this.logarithmic) {
y = d3.scale.log()
.range([chartHeight, 0])
.domain([1, d3.max(data, function(d) {
return d.pct95;
})]);
} else {
y = d3.scale.linear()
.range([chartHeight, 0])
.domain([d3.min(data, function(d) {
return d.pct95;
}), d3.max(data, function(d) {
return d.pct95;
})]);
}
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.innerTickSize(-chartHeight)
.outerTickSize(0)
.tickPadding(10);
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.innerTickSize(-chartWidth)
.outerTickSize(0)
.tickPadding(10);
var svg = d3.select(container).append('svg')
.attr('style', 'position:absolute')
.attr('class', 'interface-visible')
.attr('width', svgWidth)
.attr('height', svgHeight)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// clipping to start chart hidden and slide it in later
var rectClip = svg.append('clipPath')
.attr('id', 'rect-clip')
.append('rect')
.attr('width', 0)
.attr('height', chartHeight);
var graphLayer = svg.append('g');
var overlay = svg.append('rect')
.attr('class', 'overlay')
.attr('style', 'position:absolute; z-index:1')
.attr('width', chartWidth)
.attr('height', chartHeight);
overlay.on('click', function() {
this.handleMouseClick(event, chartWidth, x);
}.bind(this));
overlay.on('mousedown', function() {
this.dragging = true;
}.bind(this));
overlay.on('mouseup', function() {
this.dragging = false;
}.bind(this));
overlay.on('mouseleave', function() {
this.handleMouseOutGraph();
}.bind(this));
overlay.on('mousemove', function() {
this.handleMouseOverGraph(event, chartWidth, chartHeight, x);
}.bind(this));
// make sure to use offset() and not position() as we want it relative to the document, not its parent
hoverLineXOffset = margin.left + angular.element(container).offset().left;
hoverLineYOffset = margin.top + angular.element(container).offset().top;
// make sure to use offset() and not position() as we want it relative to the document, not its parent
timeIndicatorLineXOffset = margin.left + angular.element(container).offset().left;
timeIndicatorLineYOffset = margin.top + angular.element(container).offset().top;
Messagebus.subscribe('cesiumTimeSelected', function(event, value) {
this.handleTimeChange(value);
}.bind(this));
this.addAxesAndLegend(svg, xAxis, yAxis, margin, chartWidth, chartHeight);
this.drawPaths(graphLayer, data, x, y, chartWidth, chartHeight);
this.startTransitions(graphLayer, chartWidth, chartHeight, rectClip);
};
this.makeChart(data);
}.bind(this), 400);
}.bind(this);
}.bind(this));
}.bind(this);
}
angular.module('eWaterCycleApp.customgraph').controller('CustomGraphController', CustomGraphController);
})();