From c65cff0a81bfd19dbfe199747b51614da129bd9b Mon Sep 17 00:00:00 2001
From: Shiv K Sah
Date: Fri, 23 Jun 2017 21:40:09 +0530
Subject: [PATCH 1/3] Common UI chart component for all reports
---
cloud/endagaweb/static/css/dashboard.css | 19 +
.../js/dashboard/report-chart-components.js | 849 ++++++++++++++++++
2 files changed, 868 insertions(+)
create mode 100644 cloud/endagaweb/static/js/dashboard/report-chart-components.js
diff --git a/cloud/endagaweb/static/css/dashboard.css b/cloud/endagaweb/static/css/dashboard.css
index b79e59bc..bd99a031 100644
--- a/cloud/endagaweb/static/css/dashboard.css
+++ b/cloud/endagaweb/static/css/dashboard.css
@@ -504,3 +504,22 @@ div.message {
opacity: 1;
}
}
+
+/* Reports chart */
+.nv-label, .nv-legend-text {
+ text-transform: capitalize;
+}
+.nvtooltip {
+ text-transform: capitalize;
+}
+.display td {
+ text-align: center;
+}
+.display td:nth-child(1) {
+ background-color: #eee;
+ font-weight: bold;
+}
+.display td:nth-child(2) {
+ background-color: #eee;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/cloud/endagaweb/static/js/dashboard/report-chart-components.js b/cloud/endagaweb/static/js/dashboard/report-chart-components.js
new file mode 100644
index 00000000..390c6efc
--- /dev/null
+++ b/cloud/endagaweb/static/js/dashboard/report-chart-components.js
@@ -0,0 +1,849 @@
+/*
+ * Copyright (c) 2016-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+// React chart components.
+
+
+var TimeseriesChartWithButtonsAndDatePickers = React.createClass({
+ getInitialState: function() {
+ // We expect many of these values to be overridden before the chart is
+ // first rendered -- see componentDidMount.
+ return {
+ startTimeEpoch: 0,
+ endTimeEpoch: -1,
+ isLoading: true,
+ chartData: {},
+ activeButtonText: '',
+ xAxisFormatter: '%x',
+ yAxisFormatter: '',
+ activeView:''
+ }
+ },
+ getDefaultProps: function() {
+ // Load the current time with the user's clock if nothing is specified. We
+ // also specify the user's computer's timezone offset and use that to
+ // adjust the graph data.
+ var currentTime = Math.round(new Date().getTime() / 1000);
+ return {
+ title: 'title (set me!)',
+ chartID: 'one',
+ buttons: ['hour', 'day', 'week', 'month', 'year'],
+ icons: ['graph', 'list'],
+ defaultView: 'graph',
+ defaultButtonText: 'week',
+ endpoint: '/api/v1/stats/',
+ statTypes: 'sms',
+ level: 'network',
+ levelID: 0,
+ aggregation: 'count',
+ yAxisLabel: 'an axis label (set me!)',
+ currentTimeEpoch: currentTime,
+ timezoneOffset: 0,
+ tooltipUnits: '',
+ frontTooltip: '',
+ chartType: 'line-chart',
+ reportView: 'list'
+ }
+ },
+
+ // On-mount, build the charts with the default data.
+ // Note that we have to load the current time here.
+ componentDidMount: function() {
+ this.setState({
+ activeButtonText: this.props.defaultButtonText,
+ activeView: this.props.defaultView,
+ startTimeEpoch: this.props.currentTimeEpoch - secondsMap[this.props.defaultButtonText],
+ endTimeEpoch: this.props.currentTimeEpoch,
+ // When the request params in the state have been set, go get more data.
+ }, function() {
+ this.updateChartData();
+ });
+ },
+
+ componentDidUpdate(prevProps, prevState) {
+ // Update if we toggled a load
+ if (!prevState.isLoading && this.state.isLoading) {
+ this.updateChartData();
+ }
+ },
+
+ // This handler takes the text of the date range buttons
+ // and ouputs figures out the corresponding number of seconds.
+ handleButtonClick: function(text) {
+ // Update only if the startTime has actually changed.
+ var newStartTimeEpoch = this.props.currentTimeEpoch - secondsMap[text];
+ if (this.state.startTimeEpoch != newStartTimeEpoch) {
+ this.setState({
+ startTimeEpoch: newStartTimeEpoch,
+ endTimeEpoch: this.props.currentTimeEpoch,
+ isLoading: true,
+ activeButtonText: text,
+ });
+ }
+ },
+
+ // This handler takes the text of the view mode buttons
+ // and ouputs figures out the corresponding number of seconds.
+ handleViewClick: function(text) {
+ // Update only if the startTime has actually changed.
+ if (this.state.activeView != text) {
+
+ this.setState({
+ startTimeEpoch: this.state.startTimeEpoch,
+ endTimeEpoch: this.props.currentTimeEpoch,
+ isLoading: true,
+ activeView: text,
+ });
+ var interval = this.props.defaultButtonText;
+ setTimeout(function(){
+ //this.handleButtonClick(interval);
+ this.forceUpdate()
+ }.bind(this), 1000);
+
+ }
+ },
+
+ handleDownloadClick: function(text) {
+ var queryParams = {
+ 'start-time-epoch': this.state.startTimeEpoch,
+ 'end-time-epoch': this.state.endTimeEpoch,
+ 'stat-types': this.props.statTypes,
+ 'level':this.props.level,
+ 'level_id': this.props.levelID,
+ 'report-type':this.props.title,
+
+ };
+ $.get('/report/downloadcsv', queryParams, function(data,response) {
+
+ var todayTime = new Date(); var month = (todayTime .getMonth() + 1); var day = (todayTime .getDate()); var year = (todayTime .getFullYear());
+ var convertdate = year+ "-" + month + "-" +day;
+ this.setState({
+ isLoading: false,
+ data: data,
+ title:this.props.title
+ });
+ var filename = this.state.title
+ var csvData = new Blob([data], {type: 'text/csv;charset=utf-8;'});
+ var csvURL = null;
+ if (navigator.msSaveBlob) {
+ csvURL = navigator.msSaveBlob(csvData, filename+"-"+convertdate+'.csv');
+ } else {
+ csvURL = window.URL.createObjectURL(csvData);
+ }
+ var tempLink = document.createElement('a');
+ document.body.appendChild(tempLink);
+ tempLink.href = csvURL;
+ tempLink.setAttribute('download', filename+"-"+convertdate+'.csv');
+ tempLink.target="_self"
+ tempLink.click();
+ }.bind(this));
+ },
+
+ // Datepicker handlers, one each for changing the start and end times.
+ startTimeChange: function(newTime) {
+ if (newTime < this.state.endTimeEpoch && !this.state.isLoading) {
+ this.setState({
+ startTimeEpoch: newTime,
+ isLoading: true,
+ });
+ }
+ },
+ endTimeChange: function(newTime) {
+ var now = moment().unix();
+ if (newTime > this.state.startTimeEpoch && newTime < now
+ && !this.state.isLoading) {
+ this.setState({
+ endTimeEpoch: newTime,
+ isLoading: true,
+ });
+ }
+ },
+
+ // Gets new chart data from the backend.
+ // Recall that this must be called explicitly..it's a bit different
+ // than the normal react component connectivity.
+ // First figure out how to set the interval (or, more aptly, the bin width)
+ // and the x-axis formatter (see the d3 wiki on time formatting).
+ updateChartData: function() {
+ var interval, newXAxisFormatter, newYAxisFormatter;
+ var delta = this.state.endTimeEpoch - this.state.startTimeEpoch;
+ if (delta <= 60) {
+ interval = 'seconds';
+ newXAxisFormatter = '%-H:%M:%S';
+ } else if (delta <= (60 * 60)) {
+ interval = 'minutes';
+ newXAxisFormatter = '%-H:%M';
+ } else if (delta <= (24 * 60 * 60)) {
+ interval = 'hours';
+ newXAxisFormatter = '%-H:%M';
+ } else if (delta <= (7 * 24 * 60 * 60)) {
+ interval = 'hours';
+ newXAxisFormatter = '%b %d, %-H:%M';
+ } else if (delta <= (30 * 24 * 60 * 60)) {
+ interval = 'days';
+ newXAxisFormatter = '%b %d';
+ } else if (delta <= (365 * 24 * 60 * 60)) {
+ interval = 'days';
+ newXAxisFormatter = '%b %d';
+ } else {
+ interval = 'months';
+ newXAxisFormatter = '%x';
+ }
+ if (this.props.statTypes == 'total_data,uploaded_data,downloaded_data') {
+ newYAxisFormatter = '.1f';
+ } else {
+ newYAxisFormatter = '';
+ }
+ var queryParams = {
+ 'start-time-epoch': this.state.startTimeEpoch,
+ 'end-time-epoch': this.state.endTimeEpoch,
+ 'interval': interval,
+ 'stat-types': this.props.statTypes,
+ 'level-id': this.props.levelID,
+ 'aggregation': this.props.aggregation,
+ 'extras': this.props.extras,
+ 'dynamic-stat': this.props.dynamicStat,
+ 'topup-percent': this.props.topupPercent,
+ 'report-view': this.props.reportView
+ };
+ var endpoint = this.props.endpoint + this.props.level;
+ $.get(endpoint, queryParams, function(data) {
+ this.setState({
+ isLoading: false,
+ chartData: data,
+ xAxisFormatter: newXAxisFormatter,
+ yAxisFormatter: newYAxisFormatter,
+ });
+ }.bind(this));
+ },
+
+ render: function() {
+ var fromDatepickerID = 'from-datepicker-' + this.props.chartID;
+ var toDatepickerID = 'to-datepicker-' + this.props.chartID;
+ return (
+
+
{this.props.title}
+ past
+ {this.props.buttons.map(function(buttonText, index) {
+ return (
+
+ );
+ }, this)}
+
+
+
+
+
+ {this.props.icons.map(function(buttonText, index) {
+ return (
+
+ );
+ }, this)}
+
+
+
+
+
+
+ );
+ },
+});
+
+
+var secondsMap = {
+ 'hour': 60 * 60,
+ 'day': 24 * 60 * 60,
+ 'week': 7 * 24 * 60 * 60,
+ 'month': 30 * 24 * 60 * 60,
+ 'year': 365 * 24 * 60 * 60,
+};
+
+var add = function (a, b){
+ return a + b ;
+}
+
+// Builds the target chart from scratch. NVD3 surprisingly handles this well.
+// domTarget is the SVG element's parent and data is the info that will be graphed.
+var updateChart = function(domTarget, data, xAxisFormatter, yAxisFormatter, yAxisLabel, timezoneOffset, tooltipUnits, frontTooltip, chartType, domTargetId) {
+ // We pass in the timezone offset and calculate a locale offset. The former
+ // is based on the UserProfile's specified timezone and the latter is the user's
+ // computer's timezone offset. We manually shift the data to work around
+ // d3's conversion into the user's computer's timezone. Note that for my laptop
+ // in PST, the locale offset is positive (7hrs) while the UserProfile offset
+ // is negative (-7hrs).
+ var localeOffset = 60 * (new Date()).getTimezoneOffset();
+ var shiftedData = [];
+ var changeAmount = [];
+ var tableData = [];
+
+ for (var index in data) {
+ var newSeries = { 'key': data[index]['key'] };
+ var newValues = [];
+ if( typeof(data[index]['values']) === 'object'){
+ for (var series_index in data[index]['values']) {
+ var newValue = [
+ // Shift out of the locale offset to 'convert' to UTC and then shift
+ // back into the operator's tz by adding the tz offset from the server.
+ data[index]['values'][series_index][0] + 1e3 * localeOffset + 1e3 * timezoneOffset,
+ data[index]['values'][series_index][1]
+ ];
+ newValues.push(newValue);
+ changeAmount.push(newValue[1])
+ }
+ // Get sum of the total charges
+ var sumAmount = changeAmount.reduce(add, 0);
+ changeAmount = []
+ // sum can be of all negative values
+ if ( sumAmount < 0 ){
+ newSeries['total'] = (sumAmount * -1);
+ }
+ else{
+ newSeries['total'] = (sumAmount);
+ }
+ newSeries['values'] = newValues;
+ } else {
+ newSeries['total'] = data[index]['values'];
+ }
+ tableData.push([newSeries['key'], newSeries['total']]);
+ shiftedData.push(newSeries);
+ }
+
+ $('.'+domTargetId).DataTable({
+ data: tableData,
+ paging: false,
+ ordering: false,
+ info: false,
+ searching: false,
+ autoWidth: true,
+ scrollY: 320,
+ destroy: true,
+ columns: [
+ { title: "Title" },
+ { title: "Value" }
+ ]
+ });
+
+
+ nv.addGraph(function() {
+ if(chartType == 'pie-chart') {
+ var chart = nv.models.pieChart()
+ .x(function(d) { return d.key.replace('_'," "); })
+ .y(function(d) { return d.total; })
+ .showLabels(true)
+ .labelType("percent");
+
+ chart.tooltipContent(function(key, x, y) {
+ return ''+ key + '
' + ''+ '' + '' + frontTooltip + x + '
'+ '' + ''
+ });
+
+ d3.select(domTarget)
+ .datum(shiftedData)
+ .transition().duration(1200)
+ .call(chart);
+ } else if(chartType == 'bar-chart'){
+ var chart = nv.models.multiBarChart()
+ .x(function(d) { return d[0] })
+ .y(function(d) { return d[1] })
+ //.staggerLabels(true) //Too many bars and not enough room? Try staggering labels.
+ // .tooltips(true)
+ // .showValues(true) //...instead, show the bar value right on top of each bar.
+ .transitionDuration(350)
+ .stacked(false).showControls(false);
+
+ chart.xAxis.tickFormat(function(d) {
+ return d3.time.format(xAxisFormatter)(new Date(d));
+ });
+ // Fixes x-axis time alignment.
+
+ var xScale =d3.time.scale.utc();
+
+ chart.yAxis.scale(xScale)
+ .axisLabel(yAxisLabel)
+ .axisLabelDistance(25)
+ .tickFormat(d3.format(yAxisFormatter));
+ // Fixes the axis-labels being rendered out of the SVG element.
+ chart.margin({right: 80});
+ chart.tooltipContent(function(key, x, y) {
+ return '
' + frontTooltip + y + tooltipUnits + ' ' + key + '
' + '' + x + '
';
+ });
+
+
+ d3.select(domTarget)
+ .datum(shiftedData)
+ .transition().duration(350)
+ .call(chart);
+ } else {
+ var chart = nv.models.lineChart()
+ .x(function(d) { return d[0] })
+ .y(function(d) { return d[1] })
+ .color(d3.scale.category10().range())
+ .interpolate('monotone')
+ .showYAxis(true)
+ ;
+ chart.xAxis
+ .tickFormat(function(d) {
+ return d3.time.format(xAxisFormatter)(new Date(d));
+ });
+ // Fixes x-axis time alignment.
+ chart.xScale(d3.time.scale.utc());
+ chart.yAxis
+ .axisLabel(yAxisLabel)
+ .axisLabelDistance(25)
+ .tickFormat(d3.format(yAxisFormatter));
+ // Fixes the axis-labels being rendered out of the SVG element.
+ chart.margin({right: 80});
+ chart.tooltipContent(function(key, x, y) {
+ return '' + frontTooltip + y + tooltipUnits + ' ' + key + '
' + '' + x + '
';
+ });
+
+ d3.select(domTarget)
+ .datum(shiftedData)
+ .transition().duration(350)
+ .call(chart);
+ }
+ // Resize the chart on window resize.
+ nv.utils.windowResize(chart.update);
+ return chart;
+ });
+};
+
+
+var TimeseriesChart = React.createClass({
+
+ getDefaultProps: function() {
+ return {
+ chartID: 'some-chart-id',
+ chartHeight: 380,
+ data: {},
+ xAxisFormatter: '%x',
+ yAxisFormatter: '',
+ yAxisLabel: 'the y axis!',
+ timezoneOffset: 0,
+ frontTooltip: '',
+ tooltipUnits: '',
+ chartType:'',
+ activeView:''
+ }
+ },
+
+ chartIsFlat(results) {
+ return results.every(function(series) {
+ if(typeof(series['values']) === 'object'){
+ return series['values'].every(function(pair) {
+ return pair[1] === 0;
+ });
+ } else {
+ return series['values'] === 0;
+ }
+ });
+ },
+
+ render: function() {
+
+ var results = this.props.data['results'];
+ var isFlatChart = !results || this.chartIsFlat(results);
+ var className = ['time-series-chart-container'];
+ var flatLineOverlay = null;
+ if (isFlatChart) {
+ flatLineOverlay = (
+
+
+ No data available for this range.
+
+
+ );
+ className.push('flat');
+ }
+ if(this.props.activeView == 'list') {
+ $('#'+this.props.chartID + "-download").hide();
+ return (
+
+ );
+ }
+ else {
+ $('#'+this.props.chartID + "-download").show();
+ return (
+
+ {flatLineOverlay}
+
+
+ );
+ }
+ }
+});
+
+
+var TimeSeriesChartElement = React.createClass({
+ // When the request params have changed, get new data and rebuild the graph.
+ // We circumvent react's typical re-render cycle for this component by returning false.
+ shouldComponentUpdate: function(nextProps) {
+ this.props.activeView = nextProps.activeView;
+ var nextData = JSON.stringify(nextProps.data);
+ var prevData = JSON.stringify(this.props.data);
+ if (nextData !== prevData) {
+ updateChart(
+ '#' + this.props.chartID,
+ nextProps.data['results'],
+ nextProps.xAxisFormatter,
+ nextProps.yAxisFormatter,
+ nextProps.yAxisLabel,
+ this.props.timezoneOffset,
+ this.props.tooltipUnits,
+ this.props.frontTooltip,
+ this.props.chartType,
+ this.props.chartID
+ );
+ }
+ return false;
+ },
+
+ render: function() {
+ var inlineStyles = {
+ height: this.props.chartHeight
+ };
+ return (
+
+ );
+ }
+});
+
+
+var RangeButton = React.createClass({
+ propTypes: {
+ buttonText: React.PropTypes.string.isRequired
+ },
+
+ getDefaultProps: function() {
+ return {
+ buttonText: 'day',
+ activeButtonText: '',
+ onButtonClick: null,
+ }
+ },
+
+ render: function() {
+ // Determine whether this particular button is active by checking
+ // this button's text vs the owner's knowledge of the active button.
+ // Then change styles accordingly.
+ var inlineStyles = {
+ marginRight: 20
+ };
+ if (this.props.buttonText == this.props.activeButtonText) {
+ inlineStyles.cursor = 'inherit';
+ inlineStyles.color = 'black';
+ inlineStyles.textDecoration = 'none';
+ } else {
+ inlineStyles.cursor = 'pointer';
+ }
+ return (
+
+ {this.props.buttonText}
+
+ );
+ },
+
+ onThisClick: function(text) {
+ this.props.onButtonClick(text);
+ },
+});
+
+
+var LoadingText = React.createClass({
+ getDefaultProps: function() {
+ return {
+ visible: false,
+ }
+ },
+
+ render: function() {
+ var inlineStyles = {
+ display: this.props.visible ? 'inline' : 'none',
+ marginRight: 20,
+ };
+
+ return (
+
+ (loading..)
+
+ );
+ },
+});
+
+
+var DatePicker = React.createClass({
+ getDefaultProps: function() {
+ return {
+ label: 'date',
+ pickerID: 'some-datetimepicker-id',
+ epochTime: 0,
+ onDatePickerChange: null,
+ datePickerOptions : {
+ icons: {
+ time: 'fa fa-clock-o',
+ date: 'fa fa-calendar',
+ up: 'fa fa-arrow-up',
+ down: 'fa fa-arrow-down',
+ previous: 'fa fa-arrow-left',
+ next: 'fa fa-arrow-right',
+ today: 'fa fa-circle-o',
+ },
+ showTodayButton: true,
+ format: 'YYYY-MM-DD [at] h:mmA',
+ },
+ dateFormat: 'YYYY-MM-DD [at] h:mmA',
+ }
+ },
+
+ componentDidMount: function() {
+ var formattedDate = moment.unix(this.props.epochTime).format(this.props.dateFormat);
+ var domTarget = '#' + this.props.pickerID;
+ $(domTarget).keydown(function(e){
+ e.preventDefault();
+ return false;
+ });
+ $(domTarget)
+ .datetimepicker(this.props.datePickerOptions)
+ .data('DateTimePicker')
+ .date(formattedDate);
+ var dateFormat = this.props.dateFormat;
+ var handler = this.props.onDatePickerChange;
+ $(domTarget).on('dp.change', function(event) {
+ var newEpochTime = moment(event.target.value, dateFormat).unix();
+ handler(newEpochTime);
+ });
+ },
+
+ shouldComponentUpdate: function(nextProps) {
+ var formattedDate = moment.unix(nextProps.epochTime).format(nextProps.dateFormat);
+ var domTarget = '#' + nextProps.pickerID;
+ $(domTarget).data('DateTimePicker').date(formattedDate);
+ return false
+ },
+
+ render: function() {
+ return (
+
+
+
+
+ );
+ },
+});
+
+var DownloadButton = React.createClass({
+ getDefaultProps: function() {
+ return {
+ visible: false,
+ startimeepoch:'',
+ endTimeEpoch:'',
+ defaultButtonText: 'week',
+ statsType:'',
+ onButtonClick: null,
+ activeView:'graph'
+ }
+ },
+ componentWillMount() {
+ this.id = this.props.chartID + "-download";
+ },
+ componentDidMount: function() {
+ var domTargetId = this.props.chartID;
+ var btn = document.getElementById(this.id);
+ var svg = document.getElementById(domTargetId);
+ var canvas = document.querySelector('canvas');
+
+ btn.addEventListener('click', function () {
+
+ var width = $("#"+domTargetId).width();
+ var height = $("#"+domTargetId).height();
+
+ var canvas = document.getElementById('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ var ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = "#FFF";
+ ctx.fillRect(0, 0, width, height);
+
+ var data = (new XMLSerializer()).serializeToString(svg);
+ var DOMURL = window.URL || window.webkitURL;
+
+ var img = new Image();
+ var svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
+ var url = DOMURL.createObjectURL(svgBlob);
+
+ img.onload = function() {
+ ctx.drawImage(img, 0, 0);
+ DOMURL.revokeObjectURL(url);
+
+ var imgURI = canvas
+ .toDataURL('image/png')
+ .replace('image/png', 'image/octet-stream');
+
+ var evt = new MouseEvent('click', {
+ view: window,
+ bubbles: false,
+ cancelable: true
+ });
+
+ var a = document.createElement('a');
+ a.setAttribute('download', 'report.png');
+ a.setAttribute('href', imgURI);
+ a.setAttribute('target', '_blank');
+ a.dispatchEvent(evt);
+ };
+ img.src = url;
+ //img.setAttribute("src", "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(data))) );
+ });
+ },
+ render: function() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ },
+ onThisClick: function(text) {
+ this.props.onButtonClick();
+ }
+});
+
+var ViewButton = React.createClass({
+ propTypes: {
+ buttonText: React.PropTypes.string.isRequired
+ },
+
+ getDefaultProps: function() {
+ return {
+ buttonText: 'graph',
+ activeView: '',
+ onButtonClick: null,
+ }
+ },
+
+ render: function() {
+ // Determine whether this particular button is active by checking
+ // this button's text vs the owner's knowledge of the active button.
+ // Then change styles accordingly.
+ var inlineStyles = {
+ marginRight: 20
+ };
+ if (this.props.buttonText == this.props.activeView) {
+ inlineStyles.cursor = 'inherit';
+ inlineStyles.color = 'black';
+ inlineStyles.textDecoration = 'none';
+ } else {
+ inlineStyles.cursor = 'pointer';
+ }
+ if(this.props.buttonText == 'graph') {
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+ },
+
+ onThisClick: function(text) {
+ this.props.onButtonClick(text);
+ },
+});
+
+var Table = React.createClass({
+ shouldComponentUpdate: function(nextProps) {
+ this.props.activeView = nextProps.activeView;
+
+ var nextData = JSON.stringify(nextProps.data);
+ var prevData = JSON.stringify(this.props.data);
+ if (nextData !== prevData) {
+ updateChart(
+ '#' + this.props.chartID,
+ nextProps.data['results'],
+ nextProps.xAxisFormatter,
+ nextProps.yAxisFormatter,
+ nextProps.yAxisLabel,
+ this.props.timezoneOffset,
+ this.props.tooltipUnits,
+ this.props.frontTooltip,
+ this.props.chartType,
+ this.props.chartID
+ );
+ }
+ return false;
+ },
+
+ render: function() {
+ var inlineStyles = {
+ 'min-height': '360px',
+ 'margin-top': '20px'
+ };
+ return (
+
+ );
+ }
+});
From a2eb5b56fe0c768834bc94bf9d6ad049960a015e Mon Sep 17 00:00:00 2001
From: Shiv K Sah
Date: Thu, 3 Aug 2017 19:50:26 +0530
Subject: [PATCH 2/3] base files for all report
---
.../js/dashboard/report-chart-components.js | 1704 +++++++++--------
cloud/endagaweb/stats_app/stats_client.py | 174 +-
cloud/endagaweb/stats_app/views.py | 74 +-
.../endagaweb/templates/dashboard/layout.html | 35 +-
.../templates/dashboard/report/filter.html | 75 +
.../templates/dashboard/report/header.html | 19 +
.../report/health-filter-action.html | 32 +
.../templates/dashboard/report/nav.html | 46 +
cloud/endagaweb/urls.py | 17 +-
cloud/endagaweb/views/__init__.py | 1 +
cloud/endagaweb/views/reports.py | 372 ++++
11 files changed, 1724 insertions(+), 825 deletions(-)
create mode 100644 cloud/endagaweb/templates/dashboard/report/filter.html
create mode 100644 cloud/endagaweb/templates/dashboard/report/header.html
create mode 100644 cloud/endagaweb/templates/dashboard/report/health-filter-action.html
create mode 100644 cloud/endagaweb/templates/dashboard/report/nav.html
create mode 100644 cloud/endagaweb/views/reports.py
diff --git a/cloud/endagaweb/static/js/dashboard/report-chart-components.js b/cloud/endagaweb/static/js/dashboard/report-chart-components.js
index 390c6efc..c074f49f 100644
--- a/cloud/endagaweb/static/js/dashboard/report-chart-components.js
+++ b/cloud/endagaweb/static/js/dashboard/report-chart-components.js
@@ -11,297 +11,333 @@
var TimeseriesChartWithButtonsAndDatePickers = React.createClass({
- getInitialState: function() {
- // We expect many of these values to be overridden before the chart is
- // first rendered -- see componentDidMount.
- return {
- startTimeEpoch: 0,
- endTimeEpoch: -1,
- isLoading: true,
- chartData: {},
- activeButtonText: '',
- xAxisFormatter: '%x',
- yAxisFormatter: '',
- activeView:''
- }
- },
- getDefaultProps: function() {
- // Load the current time with the user's clock if nothing is specified. We
- // also specify the user's computer's timezone offset and use that to
- // adjust the graph data.
- var currentTime = Math.round(new Date().getTime() / 1000);
- return {
- title: 'title (set me!)',
- chartID: 'one',
- buttons: ['hour', 'day', 'week', 'month', 'year'],
- icons: ['graph', 'list'],
- defaultView: 'graph',
- defaultButtonText: 'week',
- endpoint: '/api/v1/stats/',
- statTypes: 'sms',
- level: 'network',
- levelID: 0,
- aggregation: 'count',
- yAxisLabel: 'an axis label (set me!)',
- currentTimeEpoch: currentTime,
- timezoneOffset: 0,
- tooltipUnits: '',
- frontTooltip: '',
- chartType: 'line-chart',
- reportView: 'list'
- }
- },
-
- // On-mount, build the charts with the default data.
- // Note that we have to load the current time here.
- componentDidMount: function() {
- this.setState({
- activeButtonText: this.props.defaultButtonText,
- activeView: this.props.defaultView,
- startTimeEpoch: this.props.currentTimeEpoch - secondsMap[this.props.defaultButtonText],
- endTimeEpoch: this.props.currentTimeEpoch,
- // When the request params in the state have been set, go get more data.
- }, function() {
- this.updateChartData();
- });
- },
-
- componentDidUpdate(prevProps, prevState) {
- // Update if we toggled a load
- if (!prevState.isLoading && this.state.isLoading) {
- this.updateChartData();
- }
- },
-
- // This handler takes the text of the date range buttons
- // and ouputs figures out the corresponding number of seconds.
- handleButtonClick: function(text) {
- // Update only if the startTime has actually changed.
- var newStartTimeEpoch = this.props.currentTimeEpoch - secondsMap[text];
- if (this.state.startTimeEpoch != newStartTimeEpoch) {
- this.setState({
- startTimeEpoch: newStartTimeEpoch,
- endTimeEpoch: this.props.currentTimeEpoch,
- isLoading: true,
- activeButtonText: text,
- });
- }
- },
-
- // This handler takes the text of the view mode buttons
- // and ouputs figures out the corresponding number of seconds.
- handleViewClick: function(text) {
- // Update only if the startTime has actually changed.
- if (this.state.activeView != text) {
-
- this.setState({
- startTimeEpoch: this.state.startTimeEpoch,
- endTimeEpoch: this.props.currentTimeEpoch,
- isLoading: true,
- activeView: text,
- });
- var interval = this.props.defaultButtonText;
- setTimeout(function(){
- //this.handleButtonClick(interval);
- this.forceUpdate()
- }.bind(this), 1000);
- }
- },
-
- handleDownloadClick: function(text) {
- var queryParams = {
- 'start-time-epoch': this.state.startTimeEpoch,
- 'end-time-epoch': this.state.endTimeEpoch,
- 'stat-types': this.props.statTypes,
- 'level':this.props.level,
- 'level_id': this.props.levelID,
- 'report-type':this.props.title,
-
- };
- $.get('/report/downloadcsv', queryParams, function(data,response) {
-
- var todayTime = new Date(); var month = (todayTime .getMonth() + 1); var day = (todayTime .getDate()); var year = (todayTime .getFullYear());
- var convertdate = year+ "-" + month + "-" +day;
- this.setState({
- isLoading: false,
- data: data,
- title:this.props.title
- });
- var filename = this.state.title
- var csvData = new Blob([data], {type: 'text/csv;charset=utf-8;'});
- var csvURL = null;
- if (navigator.msSaveBlob) {
- csvURL = navigator.msSaveBlob(csvData, filename+"-"+convertdate+'.csv');
- } else {
- csvURL = window.URL.createObjectURL(csvData);
- }
- var tempLink = document.createElement('a');
- document.body.appendChild(tempLink);
- tempLink.href = csvURL;
- tempLink.setAttribute('download', filename+"-"+convertdate+'.csv');
- tempLink.target="_self"
- tempLink.click();
- }.bind(this));
- },
-
- // Datepicker handlers, one each for changing the start and end times.
- startTimeChange: function(newTime) {
- if (newTime < this.state.endTimeEpoch && !this.state.isLoading) {
- this.setState({
- startTimeEpoch: newTime,
- isLoading: true,
- });
- }
- },
- endTimeChange: function(newTime) {
- var now = moment().unix();
- if (newTime > this.state.startTimeEpoch && newTime < now
- && !this.state.isLoading) {
- this.setState({
- endTimeEpoch: newTime,
- isLoading: true,
- });
- }
- },
-
- // Gets new chart data from the backend.
- // Recall that this must be called explicitly..it's a bit different
- // than the normal react component connectivity.
- // First figure out how to set the interval (or, more aptly, the bin width)
- // and the x-axis formatter (see the d3 wiki on time formatting).
- updateChartData: function() {
- var interval, newXAxisFormatter, newYAxisFormatter;
- var delta = this.state.endTimeEpoch - this.state.startTimeEpoch;
- if (delta <= 60) {
- interval = 'seconds';
- newXAxisFormatter = '%-H:%M:%S';
- } else if (delta <= (60 * 60)) {
- interval = 'minutes';
- newXAxisFormatter = '%-H:%M';
- } else if (delta <= (24 * 60 * 60)) {
- interval = 'hours';
- newXAxisFormatter = '%-H:%M';
- } else if (delta <= (7 * 24 * 60 * 60)) {
- interval = 'hours';
- newXAxisFormatter = '%b %d, %-H:%M';
- } else if (delta <= (30 * 24 * 60 * 60)) {
- interval = 'days';
- newXAxisFormatter = '%b %d';
- } else if (delta <= (365 * 24 * 60 * 60)) {
- interval = 'days';
- newXAxisFormatter = '%b %d';
- } else {
- interval = 'months';
- newXAxisFormatter = '%x';
- }
- if (this.props.statTypes == 'total_data,uploaded_data,downloaded_data') {
- newYAxisFormatter = '.1f';
- } else {
- newYAxisFormatter = '';
- }
- var queryParams = {
- 'start-time-epoch': this.state.startTimeEpoch,
- 'end-time-epoch': this.state.endTimeEpoch,
- 'interval': interval,
- 'stat-types': this.props.statTypes,
- 'level-id': this.props.levelID,
- 'aggregation': this.props.aggregation,
- 'extras': this.props.extras,
- 'dynamic-stat': this.props.dynamicStat,
- 'topup-percent': this.props.topupPercent,
- 'report-view': this.props.reportView
- };
- var endpoint = this.props.endpoint + this.props.level;
- $.get(endpoint, queryParams, function(data) {
- this.setState({
- isLoading: false,
- chartData: data,
- xAxisFormatter: newXAxisFormatter,
- yAxisFormatter: newYAxisFormatter,
- });
- }.bind(this));
- },
-
- render: function() {
- var fromDatepickerID = 'from-datepicker-' + this.props.chartID;
- var toDatepickerID = 'to-datepicker-' + this.props.chartID;
- return (
-
-
{this.props.title}
- past
- {this.props.buttons.map(function(buttonText, index) {
- return (
-
- );
- }, this)}
-
-
-
-
-
- {this.props.icons.map(function(buttonText, index) {
- return (
-
- );
- }, this)}
-
-
-
-
-
-
- );
- },
+ getInitialState: function() {
+ // We expect many of these values to be overridden before the chart is
+ // first rendered -- see componentDidMount.
+ return {
+ startTimeEpoch: 0,
+ endTimeEpoch: -1,
+ isLoading: true,
+ chartData: {},
+ activeButtonText: '',
+ xAxisFormatter: '%x',
+ yAxisFormatter: '',
+ activeView:'',
+ tablesColumnValueName:''
+ }
+ },
+ getDefaultProps: function() {
+ // Load the current time with the user's clock if nothing is specified. We
+ // also specify the user's computer's timezone offset and use that to
+ // adjust the graph data.
+ var currentTime = Math.round(new Date().getTime() / 1000);
+ return {
+ title: 'title (set me!)',
+ chartID: 'one',
+ buttons: ['hour', 'day', 'week', 'month', 'year'],
+ icons: ['graph', 'list'],
+ defaultView: 'graph',
+ defaultButtonText: 'week',
+ endpoint: '/api/v1/stats/',
+ statTypes: 'sms',
+ level: 'network',
+ levelID: 0,
+ aggregation: 'count',
+ yAxisLabel: 'an axis label (set me!)',
+ currentTimeEpoch: currentTime,
+ timezoneOffset: 0,
+ tooltipUnits: '',
+ frontTooltip: '',
+ chartType: 'line-chart',
+ reportView: 'list',
+ info: 'info of report type'
+ }
+ },
+
+ // On-mount, build the charts with the default data.
+ // Note that we have to load the current time here.
+ componentDidMount: function() {
+ this.setState({
+ activeButtonText: this.props.defaultButtonText,
+ activeView: this.props.defaultView,
+ startTimeEpoch: this.props.currentTimeEpoch - secondsMap[this.props.defaultButtonText],
+ endTimeEpoch: this.props.currentTimeEpoch,
+ // When the request params in the state have been set, go get more data.
+ }, function() {
+ this.updateChartData();
+ });
+ },
+ componentDidUpdate(prevProps, prevState) {
+ // Update if we toggled a load
+ if (!prevState.isLoading && this.state.isLoading) {
+ this.updateChartData();
+ }
+ },
+
+ // This handler takes the text of the date range buttons
+ // and ouputs figures out the corresponding number of seconds.
+ handleButtonClick: function(text) {
+ // Update only if the startTime has actually changed.
+ var newStartTimeEpoch = this.props.currentTimeEpoch - secondsMap[text];
+ if (this.state.startTimeEpoch != newStartTimeEpoch) {
+ this.setState({
+ startTimeEpoch: newStartTimeEpoch,
+ endTimeEpoch: this.props.currentTimeEpoch,
+ isLoading: true,
+ activeButtonText: text,
+ });
+ }
+ },
+
+ // This handler takes the text of the view mode buttons
+ handleViewClick: function(text) {
+ // Update only if the startTime has actually changed.
+ if (this.state.activeView != text) {
+ this.setState({
+ startTimeEpoch: this.state.startTimeEpoch,
+ endTimeEpoch: this.props.currentTimeEpoch,
+ isLoading: true,
+ activeView: text,
+ });
+ }
+ },
+
+ handleDownloadClick: function(text) {
+ var queryParams = {
+ 'start-time-epoch': this.state.startTimeEpoch,
+ 'end-time-epoch': this.state.endTimeEpoch,
+ 'stat-types': this.props.statTypes,
+ 'level':this.props.level,
+ 'level_id': this.props.levelID,
+ 'report-type':this.props.title,
+ };
+ $.get('/report/downloadcsv', queryParams, function(data,response) {
+ var todayTime = new Date(); var month = (todayTime .getMonth() + 1); var day = (todayTime .getDate()); var year = (todayTime .getFullYear());
+ var convertdate = year+ "-" + month + "-" +day;
+ this.setState({
+ isLoading: false,
+ data: data,
+ title:this.props.title
+ });
+ var filename = this.state.title
+ var csvData = new Blob([data], {type: 'text/csv;charset=utf-8;'});
+ var csvURL = null;
+ if (navigator.msSaveBlob) {
+ csvURL = navigator.msSaveBlob(csvData, filename+"-"+convertdate+'.csv');
+ } else {
+ csvURL = window.URL.createObjectURL(csvData);
+ }
+ var tempLink = document.createElement('a');
+ document.body.appendChild(tempLink);
+ tempLink.href = csvURL;
+ tempLink.setAttribute('download', filename+"-"+convertdate+'.csv');
+ tempLink.target="_self"
+ tempLink.click();
+ }.bind(this));
+ },
+
+ // Datepicker handlers, one each for changing the start and end times.
+ startTimeChange: function(newTime) {
+ if (newTime < this.state.endTimeEpoch && !this.state.isLoading) {
+ this.setState({
+ startTimeEpoch: newTime,
+ isLoading: true,
+ });
+ }
+ },
+ endTimeChange: function(newTime) {
+ var now = moment().unix();
+ if (newTime > this.state.startTimeEpoch && newTime < now && !this.state.isLoading) {
+ this.setState({
+ endTimeEpoch: newTime,
+ isLoading: true,
+ });
+ }
+ },
+
+ // Gets new chart data from the backend.
+ // Recall that this must be called explicitly..it's a bit different
+ // than the normal react component connectivity.
+ // First figure out how to set the interval (or, more aptly, the bin width)
+ // and the x-axis formatter (see the d3 wiki on time formatting).
+ updateChartData: function() {
+ var interval, newXAxisFormatter, newYAxisFormatter, tablesColumnValueName;
+ var delta = this.state.endTimeEpoch - this.state.startTimeEpoch;
+ if (delta <= 60) {
+ interval = 'seconds';
+ newXAxisFormatter = '%-H:%M:%S';
+ } else if (delta <= (60 * 60)) {
+ interval = 'minutes';
+ newXAxisFormatter = '%-H:%M';
+ } else if (delta <= (24 * 60 * 60)) {
+ interval = 'hours';
+ newXAxisFormatter = '%-H:%M';
+ } else if (delta <= (7 * 24 * 60 * 60)) {
+ interval = 'hours';
+ newXAxisFormatter = '%b %d, %-H:%M';
+ } else if (delta <= (30 * 24 * 60 * 60)) {
+ interval = 'days';
+ newXAxisFormatter = '%b %d';
+ } else if (delta <= (365 * 24 * 60 * 60)) {
+ interval = 'days';
+ newXAxisFormatter = '%b %d';
+ } else {
+ interval = 'months';
+ newXAxisFormatter = '%x';
+ }
+ if (this.props.statTypes == 'total_data,uploaded_data,downloaded_data') {
+ newYAxisFormatter = '.1f';
+ } else {
+ newYAxisFormatter = '';
+ }
+ if (this.props.chartID == 'data-chart') {
+ newYAxisFormatter = '.2f';
+ tablesColumnValueName = [{
+ title: "Type"
+ }, {
+ title: "Minutes"
+ }]
+
+ } else if (this.props.chartID == 'call-chart' | this.props.chartID == 'sms-chart') {
+ tablesColumnValueName = [{
+ title: "Type"
+ }, {
+ title: "Count"
+ }]
+ } else if (this.props.chartID == 'topupSubscriber-chart') {
+ newYAxisFormatter = '.2f';
+ tablesColumnValueName = [{
+ title: "Denomination Bracket"
+ }, {
+ title: "Amount in"
+ }]
+
+ } else if (this.props.chartID == 'topup-chart') {
+ newYAxisFormatter = '.2f';
+ tablesColumnValueName = [{
+ title: "Denomination Bracket"
+ }, {
+ title: "Count"
+ }]
+
+ } else if (this.props.chartID == 'load-transfer-chart') {
+ newYAxisFormatter = '.2f';
+ }
+ else {
+ tablesColumnValueName = [{
+ title: "Type"
+ }, {
+ title: "Count"
+ }]
+ }
+ var queryParams = {
+ 'start-time-epoch': this.state.startTimeEpoch,
+ 'end-time-epoch': this.state.endTimeEpoch,
+ 'interval': interval,
+ 'stat-types': this.props.statTypes,
+ 'level-id': this.props.levelID,
+ 'aggregation': this.props.aggregation,
+ 'extras': this.props.extras,
+ 'dynamic-stat': this.props.dynamicStat,
+ 'topup-percent': this.props.topupPercent,
+ 'report-view': this.props.reportView
+ };
+ var endpoint = this.props.endpoint + this.props.level;
+ $.get(endpoint, queryParams, function(data) {
+ this.setState({
+ isLoading: false,
+ chartData: data,
+ xAxisFormatter: newXAxisFormatter,
+ yAxisFormatter: newYAxisFormatter,
+ tablesColumnValueName: tablesColumnValueName,
+ });
+ }.bind(this));
+ },
+
+ render: function() {
+ var fromDatepickerID = 'from-datepicker-' + this.props.chartID;
+ var toDatepickerID = 'to-datepicker-' + this.props.chartID;
+ return (
+
+
+ {this.props.title}
+
+
+
past
+ {this.props.buttons.map(function(buttonText, index) {
+ return (
+
+ );
+ }, this)}
+
+
+
+
+
+ {this.props.icons.map(function(buttonText, index) {
+ return (
+
+ );
+ }, this)}
+
+
+
+
+
+
+ );
+ },
});
var secondsMap = {
- 'hour': 60 * 60,
- 'day': 24 * 60 * 60,
- 'week': 7 * 24 * 60 * 60,
- 'month': 30 * 24 * 60 * 60,
- 'year': 365 * 24 * 60 * 60,
+ 'hour': 60 * 60,
+ 'day': 24 * 60 * 60,
+ 'week': 7 * 24 * 60 * 60,
+ 'month': 30 * 24 * 60 * 60,
+ 'year': 365 * 24 * 60 * 60,
};
var add = function (a, b){
@@ -310,540 +346,634 @@ var add = function (a, b){
// Builds the target chart from scratch. NVD3 surprisingly handles this well.
// domTarget is the SVG element's parent and data is the info that will be graphed.
-var updateChart = function(domTarget, data, xAxisFormatter, yAxisFormatter, yAxisLabel, timezoneOffset, tooltipUnits, frontTooltip, chartType, domTargetId) {
- // We pass in the timezone offset and calculate a locale offset. The former
- // is based on the UserProfile's specified timezone and the latter is the user's
- // computer's timezone offset. We manually shift the data to work around
- // d3's conversion into the user's computer's timezone. Note that for my laptop
- // in PST, the locale offset is positive (7hrs) while the UserProfile offset
- // is negative (-7hrs).
- var localeOffset = 60 * (new Date()).getTimezoneOffset();
- var shiftedData = [];
- var changeAmount = [];
- var tableData = [];
-
- for (var index in data) {
- var newSeries = { 'key': data[index]['key'] };
- var newValues = [];
- if( typeof(data[index]['values']) === 'object'){
- for (var series_index in data[index]['values']) {
- var newValue = [
- // Shift out of the locale offset to 'convert' to UTC and then shift
- // back into the operator's tz by adding the tz offset from the server.
- data[index]['values'][series_index][0] + 1e3 * localeOffset + 1e3 * timezoneOffset,
- data[index]['values'][series_index][1]
- ];
- newValues.push(newValue);
- changeAmount.push(newValue[1])
- }
- // Get sum of the total charges
- var sumAmount = changeAmount.reduce(add, 0);
- changeAmount = []
- // sum can be of all negative values
- if ( sumAmount < 0 ){
- newSeries['total'] = (sumAmount * -1);
- }
- else{
- newSeries['total'] = (sumAmount);
- }
- newSeries['values'] = newValues;
- } else {
- newSeries['total'] = data[index]['values'];
+var updateChart = function(domTarget, data, xAxisFormatter, yAxisFormatter, yAxisLabel, timezoneOffset, tooltipUnits, frontTooltip, chartType, domTargetId, tablesColumnValueName) {
+ // We pass in the timezone offset and calculate a locale offset. The former
+ // is based on the UserProfile's specified timezone and the latter is the user's
+ // computer's timezone offset. We manually shift the data to work around
+ // d3's conversion into the user's computer's timezone. Note that for my laptop
+ // in PST, the locale offset is positive (7hrs) while the UserProfile offset
+ // is negative (-7hrs).
+ var localeOffset = 60 * (new Date()).getTimezoneOffset();
+ var shiftedData = [];
+ var changeAmount = [];
+ var tableData = [];
+
+
+ for (var index in data) {
+ var newSeries = {
+ 'key': data[index]['key']
+ };
+ var retailer = data[index]['retailer_table_data']
+
+ var newValues = [];
+ if (typeof(data[index]['values']) === 'object') {
+ for (var series_index in data[index]['values']) {
+
+ var newValue = [
+ moment(data[index]['values'][series_index][0] ),
+ data[index]['values'][series_index][1]
+ ];
+ newValues.push(newValue);
+ changeAmount.push(newValue[1])
+ }
+ // Get sum of the total charges
+ var sumAmount = changeAmount.reduce(add, 0);
+ changeAmount = []
+ // sum can be of all negative values
+ if (sumAmount < 0) {
+ newSeries['total'] = (sumAmount * -1);
+ } else {
+ newSeries['total'] = (sumAmount);
+ }
+ newSeries['values'] = newValues;
+ } else {
+ newSeries['total'] = data[index]['values'];
+ }
+ // Get sum of the total charges
+
+
+ if ( domTargetId == 'call-sms-chart' || domTargetId == 'sms-chart' || domTargetId == 'call-chart' ) {
+ if (newSeries['total'] != undefined) {
+ newSeries['total'] = newSeries['total']
+ tableData.push([newSeries['key'], newSeries['total']]);
+ }}
+ else if (domTargetId == 'data-chart' || domTargetId == 'topupSubscriber-chart' || domTargetId == 'call-billing-chart' || domTargetId == 'sms-billing-chart' || domTargetId == 'call-sms-billing-chart'){
+
+ if (newSeries['total'] != undefined) {
+ newSeries['total'] = newSeries['total'].toFixed(2);
+ tableData.push([newSeries['key'], newSeries['total']]);
+ }
+ }
+ else if (domTargetId == 'load-transfer-chart' || domTargetId == 'add-money-chart') {
+ if (typeof(retailer !== 'undefined') && Object.keys(retailer).length >= 1) {
+ if (Object.keys(retailer).length >= 1) {
+ for (var series_index in data[index]['retailer_table_data']) {
+ var elements = data[index]['retailer_table_data'][series_index];
+ tableData.push([series_index, elements])
+ }
+ }
+ } else {
+ tableData.push(['No data', 'No data'])
+ }
+ } else {
+ newSeries['total'] = newSeries['total']
+ tableData.push([newSeries['key'], newSeries['total']]);
+ }
+
+ shiftedData.push(newSeries);
+ if (frontTooltip != "" && domTargetId == 'topupSubscriber-chart') {
+ tablesColumnValueName = [{
+ title: "Denomination Bracket"
+ }, {
+ title: "Amount in (" + frontTooltip + ")"
+ }]
+ } else if (domTargetId == 'call-billing-chart' || domTargetId == 'sms-billing-chart' || domTargetId == 'call-sms-billing-chart') {
+ tablesColumnValueName = [{
+ title: "Type"
+ }, {
+ title: "Amount in (" + frontTooltip + ")"
+ }]
+ }
+ else if (frontTooltip != "" && domTargetId == 'add-money-chart' || domTargetId == 'load-transfer-chart') {
+ tablesColumnValueName = [{
+ title: "IMSI"
+ }, {
+ title: "Amount in (" + frontTooltip + ")"
+ }]
+ } else {
+ tablesColumnValueName = tablesColumnValueName
+ }
}
- tableData.push([newSeries['key'], newSeries['total']]);
- shiftedData.push(newSeries);
- }
-
- $('.'+domTargetId).DataTable({
- data: tableData,
- paging: false,
- ordering: false,
- info: false,
- searching: false,
- autoWidth: true,
- scrollY: 320,
- destroy: true,
- columns: [
- { title: "Title" },
- { title: "Value" }
- ]
- });
-
-
- nv.addGraph(function() {
- if(chartType == 'pie-chart') {
- var chart = nv.models.pieChart()
- .x(function(d) { return d.key.replace('_'," "); })
- .y(function(d) { return d.total; })
- .showLabels(true)
- .labelType("percent");
-
- chart.tooltipContent(function(key, x, y) {
- return ''+ key + '
' + ''+ '' + '' + frontTooltip + x + '
'+ '' + ''
- });
-
- d3.select(domTarget)
- .datum(shiftedData)
- .transition().duration(1200)
- .call(chart);
- } else if(chartType == 'bar-chart'){
- var chart = nv.models.multiBarChart()
- .x(function(d) { return d[0] })
- .y(function(d) { return d[1] })
- //.staggerLabels(true) //Too many bars and not enough room? Try staggering labels.
- // .tooltips(true)
- // .showValues(true) //...instead, show the bar value right on top of each bar.
- .transitionDuration(350)
- .stacked(false).showControls(false);
-
- chart.xAxis.tickFormat(function(d) {
- return d3.time.format(xAxisFormatter)(new Date(d));
- });
- // Fixes x-axis time alignment.
-
- var xScale =d3.time.scale.utc();
-
- chart.yAxis.scale(xScale)
- .axisLabel(yAxisLabel)
- .axisLabelDistance(25)
- .tickFormat(d3.format(yAxisFormatter));
- // Fixes the axis-labels being rendered out of the SVG element.
- chart.margin({right: 80});
- chart.tooltipContent(function(key, x, y) {
- return '
' + frontTooltip + y + tooltipUnits + ' ' + key + '
' + '' + x + '
';
- });
-
- d3.select(domTarget)
- .datum(shiftedData)
- .transition().duration(350)
- .call(chart);
- } else {
- var chart = nv.models.lineChart()
- .x(function(d) { return d[0] })
- .y(function(d) { return d[1] })
- .color(d3.scale.category10().range())
- .interpolate('monotone')
- .showYAxis(true)
- ;
- chart.xAxis
- .tickFormat(function(d) {
- return d3.time.format(xAxisFormatter)(new Date(d));
- });
- // Fixes x-axis time alignment.
- chart.xScale(d3.time.scale.utc());
- chart.yAxis
- .axisLabel(yAxisLabel)
- .axisLabelDistance(25)
- .tickFormat(d3.format(yAxisFormatter));
- // Fixes the axis-labels being rendered out of the SVG element.
- chart.margin({right: 80});
- chart.tooltipContent(function(key, x, y) {
- return '' + frontTooltip + y + tooltipUnits + ' ' + key + '
' + '' + x + '
';
- });
+ $('.' + domTargetId).DataTable({
+ data: tableData,
+ paging: false,
+ ordering: false,
+ info: false,
+ searching: false,
+ autoWidth: true,
+ scrollY: 320,
+ destroy: true,
+ columns: tablesColumnValueName
+ });
- d3.select(domTarget)
- .datum(shiftedData)
- .transition().duration(350)
- .call(chart);
- }
- // Resize the chart on window resize.
- nv.utils.windowResize(chart.update);
- return chart;
- });
+ nv.addGraph(function() {
+ if (chartType == 'pie-chart') {
+ var chart = nv.models.pieChart()
+ .x(function(d) {
+ return d.key.replace(/[_]/g, ' ');
+ })
+ .y(function(d) {
+ return d.total;
+ })
+ .showLabels(true)
+ .labelType("percent");
+
+ chart.tooltipContent(function(key, x, y) {
+ chart.valueFormat(d3.format(yAxisFormatter));
+ return '' + key + '
' + '' + '' + '' + frontTooltip + x + '
' + '' + ''
+ });
+
+ d3.select(domTarget)
+ .datum(shiftedData)
+ .transition().duration(350)
+ .call(chart);
+ } else if (chartType == 'bar-chart') {
+
+ var chart = nv.models.multiBarChart()
+ .x(function(d) {
+ return d[0]
+ })
+ .y(function(d) {
+ return d[1]
+ })
+ .color(d3.scale.category10().range())
+ .options({
+ interpolate: 'monotone',
+ grid:true //<== this line here
+ })
+ .showYAxis(true)
+ .stacked(false).showControls(false)
+ .reduceXTicks(true)
+ ;
+ chart.xAxis
+ .scale( d3.time.scale.utc())
+ .showMaxMin(true);
+ chart.xAxis
+ .tickFormat(function(d){return d3.time.format(xAxisFormatter)(new Date(d));})
+ chart.yAxis
+ .axisLabel(yAxisLabel)
+ .axisLabelDistance(25)
+ .tickFormat(d3.format(yAxisFormatter));
+ // Fixes the axis-labels being rendered out of the SVG element.
+ chart.margin({
+ right: 90
+ });
+ chart.tooltipContent(function(key, x, y) {
+ if (key == 'add_money') {
+ return '
' + frontTooltip + y + tooltipUnits + ' ' + '
' + '' + x + '
';
+ } else {
+ return '' + frontTooltip + y + tooltipUnits + ' ' + key + '
' + '' + x + '
';
+ }
+ });
+ d3.select(domTarget)
+ .datum(shiftedData)
+ .transition().duration(350)
+ .call(chart);
+
+ } else {
+ var chart = nv.models.lineChart()
+ .x(function(d) {
+ return d[0]
+ })
+ .y(function(d) {
+ if (d[1] > 0){
+ return 1
+ } else {
+ return 0
+ }
+
+ })
+ .color(d3.scale.category10().range())
+ .interpolate('monotone')
+ .showYAxis(true)
+ ;
+ chart.xAxis
+ .tickFormat(function(d) {
+ return d3.time.format(xAxisFormatter)(new Date(d));
+ });
+ // Fixes x-axis time alignment.
+ chart.xScale(d3.time.scale.utc());
+ // For Health Graph
+ if(domTarget == '#call-chart') {
+ chart.yAxis
+ .axisLabel(yAxisLabel)
+ .axisLabelDistance(25)
+ .tickFormat(function(d) {
+ if (d == 1){
+ return "UP";
+ } else if (d > 0) {
+ return ;
+ } else {
+ return "DOWN";
+ }
+ });
+ } else {
+ chart.yAxis
+ .axisLabel(yAxisLabel)
+ .axisLabelDistance(25)
+ .tickFormat(d3.format(yAxisFormatter));
+ }
+
+ // Fixes the axis-labels being rendered out of the SVG element.
+ chart.margin({
+ right: 80
+ });
+ chart.tooltipContent(function(key, x, y) {
+ if (key == 'health_state') {
+ key = ''
+ if (y =='UP'){
+ y = 'UP'
+ } else {
+ y = 'DOWN'
+ }
+ }
+ return '' + frontTooltip + y + tooltipUnits + ' ' + key + '
' + '' + x + '
';
+ });
+
+ d3.selectAll(".nv-axis path").style({ 'fill':'none', 'stroke': '#000' });
+ d3.selectAll(".nv-chart path").style({ 'fill':'none'});
+ d3.selectAll(".nv-line").style({ 'fill':'none'});
+
+ d3.select(domTarget)
+ .datum(shiftedData)
+ .transition().duration(350)
+ .call(chart);
+ }
+ // Resize the chart on window resize.
+ nv.utils.windowResize(chart.update);
+ return chart;
+ });
};
var TimeseriesChart = React.createClass({
- getDefaultProps: function() {
- return {
- chartID: 'some-chart-id',
- chartHeight: 380,
- data: {},
- xAxisFormatter: '%x',
- yAxisFormatter: '',
- yAxisLabel: 'the y axis!',
- timezoneOffset: 0,
- frontTooltip: '',
- tooltipUnits: '',
- chartType:'',
- activeView:''
- }
- },
-
- chartIsFlat(results) {
- return results.every(function(series) {
- if(typeof(series['values']) === 'object'){
- return series['values'].every(function(pair) {
- return pair[1] === 0;
+ getDefaultProps: function() {
+ return {
+ chartID: 'some-chart-id',
+ chartHeight: 380,
+ data: {},
+ xAxisFormatter: '%x',
+ yAxisFormatter: '',
+ yAxisLabel: 'the y axis!',
+ timezoneOffset: 0,
+ frontTooltip: '',
+ tooltipUnits: '',
+ chartType:'',
+ activeView:'',
+ tablesColumnValueName:''
+ }
+ },
+
+ chartIsFlat(results) {
+ return results.every(function(series) {
+ if(typeof(series['values']) === 'object'){
+ return series['values'].every(function(pair) {
+ return pair[1] === 0;
+ });
+ } else {
+ return series['values'] === 0;
+ }
});
- } else {
- return series['values'] === 0;
- }
- });
- },
-
- render: function() {
-
- var results = this.props.data['results'];
- var isFlatChart = !results || this.chartIsFlat(results);
- var className = ['time-series-chart-container'];
- var flatLineOverlay = null;
- if (isFlatChart) {
- flatLineOverlay = (
-
-
- No data available for this range.
-
-
- );
- className.push('flat');
- }
- if(this.props.activeView == 'list') {
- $('#'+this.props.chartID + "-download").hide();
- return (
-
- );
- }
- else {
- $('#'+this.props.chartID + "-download").show();
- return (
-
- {flatLineOverlay}
-
-
- );
+ },
+
+ render: function() {
+ var results = this.props.data['results'];
+ var isFlatChart = !results || this.chartIsFlat(results);
+ var className = ['time-series-chart-container'];
+ var flatLineOverlay = null;
+ if (isFlatChart) {
+ flatLineOverlay = (
+
+
+ No data available for this range.
+
+
+ );
+ className.push('flat');
+ }
+ if(this.props.activeView == 'list') {
+ $('#'+this.props.chartID + "-download").hide();
+ return (
+
+ );
+ } else {
+ $('#'+this.props.chartID + "-download").show();
+ return (
+
+ {flatLineOverlay}
+
+
+ );
+ }
}
- }
});
var TimeSeriesChartElement = React.createClass({
- // When the request params have changed, get new data and rebuild the graph.
- // We circumvent react's typical re-render cycle for this component by returning false.
- shouldComponentUpdate: function(nextProps) {
- this.props.activeView = nextProps.activeView;
- var nextData = JSON.stringify(nextProps.data);
- var prevData = JSON.stringify(this.props.data);
- if (nextData !== prevData) {
- updateChart(
- '#' + this.props.chartID,
- nextProps.data['results'],
- nextProps.xAxisFormatter,
- nextProps.yAxisFormatter,
- nextProps.yAxisLabel,
- this.props.timezoneOffset,
- this.props.tooltipUnits,
- this.props.frontTooltip,
- this.props.chartType,
- this.props.chartID
- );
+ // When the request params have changed, get new data and rebuild the graph.
+ // We circumvent react's typical re-render cycle for this component by returning false.
+ shouldComponentUpdate: function(nextProps) {
+ this.props.activeView = nextProps.activeView;
+ var nextData = JSON.stringify(nextProps.data);
+ var prevData = JSON.stringify(this.props.data);
+ this.forceUpdate();
+ if (nextData !== prevData) {
+ updateChart(
+ '#' + this.props.chartID,
+ nextProps.data['results'],
+ nextProps.xAxisFormatter,
+ nextProps.yAxisFormatter,
+ nextProps.yAxisLabel,
+ this.props.timezoneOffset,
+ this.props.tooltipUnits,
+ this.props.frontTooltip,
+ this.props.chartType,
+ this.props.chartID,
+ nextProps.tablesColumnValueName
+ );
+ }
+ return false;
+ },
+ componentDidUpdate(prevProps, prevState) {
+ var nextProps = prevProps;
+ // Update if we toggled a load
+ updateChart(
+ '#' + this.props.chartID,
+ nextProps.data['results'],
+ nextProps.xAxisFormatter,
+ nextProps.yAxisFormatter,
+ nextProps.yAxisLabel,
+ this.props.timezoneOffset,
+ this.props.tooltipUnits,
+ this.props.frontTooltip,
+ this.props.chartType,
+ this.props.chartID,
+ nextProps.tablesColumnValueName
+ );
+ },
+ render: function() {
+ var inlineStyles = {
+ height: this.props.chartHeight
+ };
+ return (
+
+ );
}
- return false;
- },
-
- render: function() {
- var inlineStyles = {
- height: this.props.chartHeight
- };
- return (
-
- );
- }
});
-
var RangeButton = React.createClass({
- propTypes: {
- buttonText: React.PropTypes.string.isRequired
- },
-
- getDefaultProps: function() {
- return {
- buttonText: 'day',
- activeButtonText: '',
- onButtonClick: null,
- }
- },
-
- render: function() {
- // Determine whether this particular button is active by checking
- // this button's text vs the owner's knowledge of the active button.
- // Then change styles accordingly.
- var inlineStyles = {
- marginRight: 20
- };
- if (this.props.buttonText == this.props.activeButtonText) {
- inlineStyles.cursor = 'inherit';
- inlineStyles.color = 'black';
- inlineStyles.textDecoration = 'none';
- } else {
- inlineStyles.cursor = 'pointer';
- }
- return (
-
- {this.props.buttonText}
-
- );
- },
-
- onThisClick: function(text) {
- this.props.onButtonClick(text);
- },
+ propTypes: {
+ buttonText: React.PropTypes.string.isRequired
+ },
+
+ getDefaultProps: function() {
+ return {
+ buttonText: 'day',
+ activeButtonText: '',
+ onButtonClick: null,
+ }
+ },
+
+ render: function() {
+ // Determine whether this particular button is active by checking
+ // this button's text vs the owner's knowledge of the active button.
+ // Then change styles accordingly.
+ var inlineStyles = {
+ marginRight: 20
+ };
+ if (this.props.buttonText == this.props.activeButtonText) {
+ inlineStyles.cursor = 'inherit';
+ inlineStyles.color = 'black';
+ inlineStyles.textDecoration = 'none';
+ } else {
+ inlineStyles.cursor = 'pointer';
+ }
+ return (
+
+ {this.props.buttonText}
+
+ );
+ },
+ onThisClick: function(text) {
+ this.props.onButtonClick(text);
+ },
});
-
var LoadingText = React.createClass({
- getDefaultProps: function() {
- return {
- visible: false,
- }
- },
-
- render: function() {
- var inlineStyles = {
- display: this.props.visible ? 'inline' : 'none',
- marginRight: 20,
- };
-
- return (
-
- (loading..)
-
- );
- },
+ getDefaultProps: function() {
+ return {
+ visible: false,
+ }
+ },
+ render: function() {
+ var inlineStyles = {
+ display: this.props.visible ? 'inline' : 'none', marginRight: 20
+ };
+ return (
+
+ (loading..)
+
+ );
+ },
});
-
var DatePicker = React.createClass({
- getDefaultProps: function() {
- return {
- label: 'date',
- pickerID: 'some-datetimepicker-id',
- epochTime: 0,
- onDatePickerChange: null,
- datePickerOptions : {
- icons: {
- time: 'fa fa-clock-o',
- date: 'fa fa-calendar',
- up: 'fa fa-arrow-up',
- down: 'fa fa-arrow-down',
- previous: 'fa fa-arrow-left',
- next: 'fa fa-arrow-right',
- today: 'fa fa-circle-o',
- },
- showTodayButton: true,
- format: 'YYYY-MM-DD [at] h:mmA',
- },
- dateFormat: 'YYYY-MM-DD [at] h:mmA',
- }
- },
-
- componentDidMount: function() {
- var formattedDate = moment.unix(this.props.epochTime).format(this.props.dateFormat);
- var domTarget = '#' + this.props.pickerID;
- $(domTarget).keydown(function(e){
- e.preventDefault();
- return false;
- });
- $(domTarget)
- .datetimepicker(this.props.datePickerOptions)
- .data('DateTimePicker')
- .date(formattedDate);
- var dateFormat = this.props.dateFormat;
- var handler = this.props.onDatePickerChange;
- $(domTarget).on('dp.change', function(event) {
- var newEpochTime = moment(event.target.value, dateFormat).unix();
- handler(newEpochTime);
- });
- },
-
- shouldComponentUpdate: function(nextProps) {
- var formattedDate = moment.unix(nextProps.epochTime).format(nextProps.dateFormat);
- var domTarget = '#' + nextProps.pickerID;
- $(domTarget).data('DateTimePicker').date(formattedDate);
- return false
- },
-
- render: function() {
- return (
-
-
-
-
- );
- },
+ getDefaultProps: function() {
+ return {
+ label: 'date',
+ pickerID: 'some-datetimepicker-id',
+ epochTime: 0,
+ onDatePickerChange: null,
+ datePickerOptions: {
+ icons: {
+ time: 'fa fa-clock-o',
+ date: 'fa fa-calendar',
+ up: 'fa fa-arrow-up',
+ down: 'fa fa-arrow-down',
+ previous: 'fa fa-arrow-left',
+ next: 'fa fa-arrow-right',
+ today: 'fa fa-circle-o',
+ },
+ showTodayButton: true,
+ format: 'YYYY-MM-DD [at] h:mmA',
+ },
+ dateFormat: 'YYYY-MM-DD [at] h:mmA',
+ }
+ },
+
+ componentDidMount: function() {
+ var formattedDate = moment.unix(this.props.epochTime).format(this.props.dateFormat);
+ var domTarget = '#' + this.props.pickerID;
+ $(domTarget).keydown(function(e) {
+ e.preventDefault();
+ return false;
+ });
+ $(domTarget)
+ .datetimepicker(this.props.datePickerOptions)
+ .data('DateTimePicker')
+ .date(formattedDate);
+ var dateFormat = this.props.dateFormat;
+ var handler = this.props.onDatePickerChange;
+ $(domTarget).on('dp.change', function(event) {
+ var newEpochTime = moment(event.target.value, dateFormat).unix();
+ handler(newEpochTime);
+ });
+ },
+
+ shouldComponentUpdate: function(nextProps) {
+ var formattedDate = moment.unix(nextProps.epochTime).format(nextProps.dateFormat);
+ var domTarget = '#' + nextProps.pickerID;
+ $(domTarget).data('DateTimePicker').date(formattedDate);
+ return false
+ },
+
+ render: function() {
+ return (
+
+
+
+
+ );
+ },
});
var DownloadButton = React.createClass({
- getDefaultProps: function() {
- return {
- visible: false,
- startimeepoch:'',
- endTimeEpoch:'',
- defaultButtonText: 'week',
- statsType:'',
- onButtonClick: null,
- activeView:'graph'
+ getDefaultProps: function() {
+ return {
+ visible: false,
+ startimeepoch:'',
+ endTimeEpoch:'',
+ defaultButtonText: 'week',
+ statsType:'',
+ onButtonClick: null,
+ activeView:'graph'
+ }
+ },
+ componentWillMount() {
+ this.id = this.props.chartID + "-download";
+ },
+ componentDidMount: function() {
+ var domTargetId = this.props.chartID;
+ var btn = document.getElementById(this.id);
+ var filename = this.props.reporttype+".png";
+
+ btn.addEventListener('click', function () {
+ saveSvgAsPng(document.getElementById(domTargetId), filename, {backgroundColor:"#FFF"});
+ });
+ },
+ render: function() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ },
+ onThisClick: function(text) {
+ this.props.onButtonClick();
}
- },
- componentWillMount() {
- this.id = this.props.chartID + "-download";
- },
- componentDidMount: function() {
- var domTargetId = this.props.chartID;
- var btn = document.getElementById(this.id);
- var svg = document.getElementById(domTargetId);
- var canvas = document.querySelector('canvas');
-
- btn.addEventListener('click', function () {
-
- var width = $("#"+domTargetId).width();
- var height = $("#"+domTargetId).height();
-
- var canvas = document.getElementById('canvas');
- canvas.width = width;
- canvas.height = height;
- var ctx = canvas.getContext('2d');
-
- ctx.fillStyle = "#FFF";
- ctx.fillRect(0, 0, width, height);
-
- var data = (new XMLSerializer()).serializeToString(svg);
- var DOMURL = window.URL || window.webkitURL;
-
- var img = new Image();
- var svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
- var url = DOMURL.createObjectURL(svgBlob);
-
- img.onload = function() {
- ctx.drawImage(img, 0, 0);
- DOMURL.revokeObjectURL(url);
-
- var imgURI = canvas
- .toDataURL('image/png')
- .replace('image/png', 'image/octet-stream');
-
- var evt = new MouseEvent('click', {
- view: window,
- bubbles: false,
- cancelable: true
- });
-
- var a = document.createElement('a');
- a.setAttribute('download', 'report.png');
- a.setAttribute('href', imgURI);
- a.setAttribute('target', '_blank');
- a.dispatchEvent(evt);
- };
- img.src = url;
- //img.setAttribute("src", "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(data))) );
- });
- },
- render: function() {
- return (
-
-
-
-
-
-
-
-
- );
- },
- onThisClick: function(text) {
- this.props.onButtonClick();
- }
});
var ViewButton = React.createClass({
- propTypes: {
- buttonText: React.PropTypes.string.isRequired
- },
-
- getDefaultProps: function() {
- return {
- buttonText: 'graph',
- activeView: '',
- onButtonClick: null,
- }
- },
-
- render: function() {
- // Determine whether this particular button is active by checking
- // this button's text vs the owner's knowledge of the active button.
- // Then change styles accordingly.
- var inlineStyles = {
- marginRight: 20
- };
- if (this.props.buttonText == this.props.activeView) {
- inlineStyles.cursor = 'inherit';
- inlineStyles.color = 'black';
- inlineStyles.textDecoration = 'none';
- } else {
- inlineStyles.cursor = 'pointer';
- }
- if(this.props.buttonText == 'graph') {
- return (
-
-
-
- );
- } else {
- return (
-
-
-
- );
- }
- },
-
- onThisClick: function(text) {
- this.props.onButtonClick(text);
- },
+ propTypes: {
+ buttonText: React.PropTypes.string.isRequired
+ },
+
+ getDefaultProps: function() {
+ return {
+ buttonText: 'graph',
+ activeView: '',
+ onButtonClick: null,
+ }
+ },
+
+ render: function() {
+ // Determine whether this particular button is active by checking
+ // this button's text vs the owner's knowledge of the active button.
+ // Then change styles accordingly.
+ var inlineStyles = {
+ marginRight: 20
+ };
+ if (this.props.buttonText == this.props.activeView) {
+ inlineStyles.cursor = 'inherit';
+ inlineStyles.color = 'black';
+ inlineStyles.textDecoration = 'none';
+ } else {
+ inlineStyles.cursor = 'pointer';
+ }
+ if(this.props.buttonText == 'graph') {
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+ },
+
+ onThisClick: function(text) {
+ this.props.onButtonClick(text);
+ },
});
var Table = React.createClass({
- shouldComponentUpdate: function(nextProps) {
- this.props.activeView = nextProps.activeView;
-
- var nextData = JSON.stringify(nextProps.data);
- var prevData = JSON.stringify(this.props.data);
- if (nextData !== prevData) {
- updateChart(
- '#' + this.props.chartID,
- nextProps.data['results'],
- nextProps.xAxisFormatter,
- nextProps.yAxisFormatter,
- nextProps.yAxisLabel,
- this.props.timezoneOffset,
- this.props.tooltipUnits,
- this.props.frontTooltip,
- this.props.chartType,
- this.props.chartID
- );
+ shouldComponentUpdate: function(nextProps) {
+ this.props.activeView = nextProps.activeView;
+
+ var nextData = JSON.stringify(nextProps.data);
+ var prevData = JSON.stringify(this.props.data);
+ this.forceUpdate();
+ if (nextData !== prevData) {
+ updateChart(
+ '#' + this.props.chartID,
+ nextProps.data['results'],
+ nextProps.xAxisFormatter,
+ nextProps.yAxisFormatter,
+ nextProps.yAxisLabel,
+ this.props.timezoneOffset,
+ this.props.tooltipUnits,
+ this.props.frontTooltip,
+ this.props.chartType,
+ this.props.chartID,
+ nextProps.tablesColumnValueName
+ );
+ }
+ return false;
+ },
+ componentDidUpdate(prevProps, prevState) {
+ var nextProps = prevProps;
+ // Update if we toggled a load
+ updateChart(
+ '#' + this.props.chartID,
+ nextProps.data['results'],
+ nextProps.xAxisFormatter,
+ nextProps.yAxisFormatter,
+ nextProps.yAxisLabel,
+ this.props.timezoneOffset,
+ this.props.tooltipUnits,
+ this.props.frontTooltip,
+ this.props.chartType,
+ this.props.chartID,
+ nextProps.tablesColumnValueName
+ );
+ },
+
+ render: function() {
+ var inlineStyles = {
+ 'min-height': '360px',
+ 'margin-top': '20px'
+ };
+ return (
+
+ );
}
- return false;
- },
-
- render: function() {
- var inlineStyles = {
- 'min-height': '360px',
- 'margin-top': '20px'
- };
- return (
-
- );
- }
});
diff --git a/cloud/endagaweb/stats_app/stats_client.py b/cloud/endagaweb/stats_app/stats_client.py
index 5ebe043f..f88f2040 100644
--- a/cloud/endagaweb/stats_app/stats_client.py
+++ b/cloud/endagaweb/stats_app/stats_client.py
@@ -8,16 +8,19 @@
of patent rights can be found in the PATENTS file in the same directory.
"""
-from datetime import datetime
import time
+from datetime import datetime, timedelta
+from operator import itemgetter
+import calendar
-from django.db.models import aggregates
-from django.db.models import Q
import pytz
import qsstats
-
+from dateutil.rrule import rrule, MONTHLY
+from django.db.models import Q
+from django.db.models import aggregates
from endagaweb import models
-
+from decimal import *
+from pytz import timezone
CALL_KINDS = [
'local_call', 'local_recv_call', 'outside_call', 'incoming_call',
@@ -25,9 +28,21 @@
SMS_KINDS = [
'local_sms', 'local_recv_sms', 'outside_sms', 'incoming_sms', 'free_sms',
'error_sms']
-USAGE_EVENT_KINDS = CALL_KINDS + SMS_KINDS + ['gprs']
+SUBSCRIBER_KINDS = ['Provisioned', 'deactivate_number']
+ZERO_BALANCE_SUBSCRIBER = ['zero_balance_subscriber']
+INACTIVE_SUBSCRIBER = ['expired', 'first_expired', 'blocked']
+BTS_STATUS = ['health_state']
+TRANSFER_KINDS = ['transfer', 'add_money']
+WATERFALL_KINDS = ['loader', 'reload_rate', 'reload_amount',
+ 'reload_transaction', 'average_load', 'average_frequency']
+DENOMINATION_KINDS = ['start_amount', 'end_amount']
+BTS_KINDS = ['bts up', 'bts down']
+USAGE_EVENT_KINDS = CALL_KINDS + SMS_KINDS + ['gprs'] + SUBSCRIBER_KINDS + \
+ TRANSFER_KINDS + WATERFALL_KINDS
TIMESERIES_STAT_KEYS = [
- 'ccch_sdcch4_load', 'tch_f_max', 'tch_f_load', 'sdcch8_max', 'tch_f_pdch_load', 'tch_f_pdch_max', 'tch_h_load', 'tch_h_max', 'sdcch8_load', 'ccch_sdcch4_max',
+ 'ccch_sdcch4_load', 'tch_f_max', 'tch_f_load', 'sdcch8_max',
+ 'tch_f_pdch_load', 'tch_f_pdch_max', 'tch_h_load', 'tch_h_max',
+ 'sdcch8_load', 'ccch_sdcch4_max',
'sdcch_load', 'sdcch_available', 'tchf_load', 'tchf_available',
'pch_active', 'pch_total', 'agch_active', 'agch_pending',
'gprs_current_pdchs', 'gprs_utilization_percentage', 'noise_rssi_db',
@@ -99,6 +114,9 @@ def aggregate_timeseries(self, param, **kwargs):
end_time_epoch = kwargs.pop('end_time_epoch', -1)
interval = kwargs.pop('interval', 'months')
aggregation = kwargs.pop('aggregation', 'count')
+ report_view = kwargs.pop('report_view', 'list')
+ imsi_dict = {}
+ imsi_list = []
# Turn the start and end epoch timestamps into datetimes.
start = datetime.fromtimestamp(start_time_epoch, pytz.utc)
if end_time_epoch != -1:
@@ -107,24 +125,44 @@ def aggregate_timeseries(self, param, **kwargs):
end = datetime.fromtimestamp(time.time(), pytz.utc)
# Build the queryset -- first determine if we're dealing with
# UsageEvents or TimeseriesStats.
- filters = None
if param in USAGE_EVENT_KINDS:
objects = models.UsageEvent.objects
filters = Q(kind=param)
+ elif param in ZERO_BALANCE_SUBSCRIBER:
+ objects = models.UsageEvent.objects
+ filters = Q(oldamt__gt=0, newamt__lte=0)
+ elif param in INACTIVE_SUBSCRIBER:
+ aggregation = 'valid_through'
+ objects = models.Subscriber.objects
+ filters = Q(state=param)
elif param in TIMESERIES_STAT_KEYS:
objects = models.TimeseriesStat.objects
filters = Q(key=param)
+ elif param in BTS_KINDS:
+ objects = models.SystemEvent.objects
+ filters = Q(type=param, bts_id=self.level_id)
+ else:
+ # For Dynamic Kinds coming from Database currently for Top Up
+ objects = models.UsageEvent.objects
+ filters = Q(kind='transfer')
# Filter by infrastructure level.
if self.level == 'tower':
filters = filters & Q(bts__id=self.level_id)
- elif self.level == 'network':
+ elif self.level == 'network' and param not in BTS_KINDS:
filters = filters & Q(network__id=self.level_id)
elif self.level == 'global':
pass
- # Create the queryset itself.
+ if kwargs.has_key('query'):
+ filters = filters & kwargs.pop('query')
+ if report_view == 'value':
+ filters = filters & Q(date__lte=end) & Q(date__gte=start)
+ result = models.UsageEvent.objects.filter(filters).values_list(
+ 'subscriber_id', flat=True).distinct()
+ return list(result)
queryset = objects.filter(filters)
+
# Use qsstats to aggregate the queryset data on an interval.
- if aggregation == 'duration':
+ if aggregation in ['duration', 'duration_minute']:
queryset_stats = qsstats.QuerySetStats(
queryset, 'date', aggregate=aggregates.Sum('billsec'))
elif aggregation == 'up_byte_count':
@@ -136,24 +174,124 @@ def aggregate_timeseries(self, param, **kwargs):
elif aggregation == 'average_value':
queryset_stats = qsstats.QuerySetStats(
queryset, 'date', aggregate=aggregates.Avg('value'))
+ elif aggregation == 'valid_through':
+ queryset_stats = qsstats.QuerySetStats(queryset, 'valid_through')
+ elif aggregation == 'reload_transcation_count':
+ queryset_stats = qsstats.QuerySetStats(
+ queryset, 'date', aggregate=(aggregates.Count('to_number')))
+ elif aggregation == 'reload_transcation_sum':
+ queryset_stats = qsstats.QuerySetStats(
+ queryset, 'date',
+ aggregate=(aggregates.Sum('change') * 0.00001))
+ # Sum of change in amounts for SMS/CALL
+ elif aggregation in ['transaction_sum', 'transcation_count']:
+ # Change is negative value, set positive for charts
+ if report_view == 'summary':
+ adjust = -10
+ elif param == 'add_money':
+ adjust = 0.00001
+ elif param == 'transfer':
+ adjust = -0.00001
+ else:
+ adjust = 1
+ queryset_stats = qsstats.QuerySetStats(
+ queryset, 'date', aggregate=(aggregates.Sum('change') * adjust))
+
+ if report_view =='table_view':
+ imsi = {}
+ for qs in queryset_stats.qs.filter(
+ date__range=(str(start), str(end))):
+ if qs.subscriber.imsi in imsi:
+ imsi[qs.subscriber.imsi] += round(qs.change * adjust, 2)
+ else:
+ imsi[qs.subscriber.imsi] = round(qs.change * adjust, 2)
+ return imsi
+
+ # if percentage is set for top top-up
+ percentage = kwargs['topup_percent']
+ top_numbers = 1
+ if percentage is not None:
+ numbers = {}
+ percentage = float(percentage) / 100
+ # Create subscribers dict
+ for query in queryset:
+ numbers[query.to_number] = 0
+ for query in queryset_stats.qs:
+ numbers[query.to_number] += (query.change * -1)
+ top_numbers = int(len(numbers) * percentage)
+ top_numbers = top_numbers if top_numbers > 1 else top_numbers
+ top_subscribers = list(dict(
+ sorted(numbers.iteritems(), key=itemgetter(1),
+ reverse=True)[:top_numbers]).keys())
+ queryset = queryset_stats.qs.filter(
+ Q(to_number__in=top_subscribers))
+ # Count the numbers
+ if aggregation == 'transcation_count':
+ queryset_stats = qsstats.QuerySetStats(
+ queryset, 'date', aggregate=(
+ aggregates.Count('to_number')))
+ else:
+ # Sum of change
+ queryset_stats = qsstats.QuerySetStats(
+ queryset, 'date', aggregate=(
+ aggregates.Sum('change')))
+ elif aggregation == 'loader':
+ queryset_stats = qsstats.QuerySetStats(
+ queryset, 'date', aggregate=aggregates.Count('subscriber_id'))
else:
queryset_stats = qsstats.QuerySetStats(queryset, 'date')
- timeseries = queryset_stats.time_series(start, end, interval=interval)
- # The timeseries results is a list of (datetime, value) pairs. We need
+
+ if param =='bts down'or param=='bts up':
+ timeseries = queryset_stats.time_series(start, end, interval='minutes')
+ else:
+ timeseries = queryset_stats.time_series(start, end,
+ interval=interval)
+
+ # The timeseries results is a list of (datetime, value) pairs. We need
# to convert the datetimes to timestamps with millisecond precision and
# then zip the pairs back together.
+ timeseries = queryset_stats.time_series(start, end,
+ interval=interval)
+ if param in BTS_KINDS:
+ timeseries = queryset_stats.time_series(start, end,
+ date_field='date',
+ interval='minutes')
+ for idx, val in enumerate(timeseries):
+ # remove unwanted objects count
+ if val[1] == 0:
+ timeseries.pop(idx)
datetimes, values = zip(*timeseries)
+ if report_view == 'summary':
+ # Return sum count for pie-chart and table view
+ if aggregation == 'transaction_sum':
+ # When kind is change
+ return sum(values) * 0.000001
+ elif aggregation == 'duration_minute':
+ return (sum(values) / 60.00) or 0
+ else:
+ return sum(values)
+
timestamps = [
int(time.mktime(dt.timetuple()) * 1e3 + dt.microsecond / 1e3)
for dt in datetimes
]
# Round the stats values when necessary.
rounded_values = []
- for value in values:
- if round(value) != round(value, 2):
- rounded_values.append(round(value, 2))
- else:
- rounded_values.append(value)
+ if param in BTS_KINDS:
+ for value in values:
+ if param == 'bts down':
+ val = -1 if value == 1 else 0
+ elif param == 'bts up':
+ val = 1 if value == 1 else 0
+ rounded_values.append(val)
+ else:
+ for value in values:
+ if value <= 0:
+ rounded_values.append(value * -0.00001)
+ elif round(value) != round(value, 2):
+ rounded_values.append(round(value, 2))
+ else:
+ rounded_values.append(value)
return zip(timestamps, rounded_values)
diff --git a/cloud/endagaweb/stats_app/views.py b/cloud/endagaweb/stats_app/views.py
index df6c88df..ce911ff5 100644
--- a/cloud/endagaweb/stats_app/views.py
+++ b/cloud/endagaweb/stats_app/views.py
@@ -14,10 +14,17 @@
from rest_framework import response
from rest_framework import status
from rest_framework import views
+import pytz
+from rest_framework.authtoken.models import Token
+import stripe
+from guardian.shortcuts import get_objects_for_user
+from ccm.common.currency import parse_credits, humanize_credits, \
+ CURRENCIES, Money
from endagaweb.stats_app import stats_client
-
+from endagaweb.models import (UserProfile, Ledger, Subscriber, UsageEvent,
+ Network, PendingCreditUpdate, Number)
# Set the stat types that we can query for. Note that some of these kinds are
# 'faux kinds' in that the stats client aggregates the true UsageEvent kinds
# for these other categories: sms, call, uploaded_data, downloaded_data,
@@ -26,13 +33,25 @@
CALL_KINDS = stats_client.CALL_KINDS + ['call']
GPRS_KINDS = ['total_data', 'uploaded_data', 'downloaded_data']
TIMESERIES_STAT_KEYS = stats_client.TIMESERIES_STAT_KEYS
-VALID_STATS = SMS_KINDS + CALL_KINDS + GPRS_KINDS + TIMESERIES_STAT_KEYS
+SUBSCRIBER_KINDS = stats_client.SUBSCRIBER_KINDS + \
+ stats_client.ZERO_BALANCE_SUBSCRIBER + \
+ stats_client.INACTIVE_SUBSCRIBER
+BTS_STATUS = stats_client.BTS_STATUS
+WATERFALL_KINDS = ['loader', 'reload_rate', 'reload_amount',
+ 'reload_transaction', 'average_load', 'average_frequency']
+NON_LOADER_KINDS = ['non_loader_base', 'cumulative_base']
+DENOMINATION_KINDS = stats_client.DENOMINATION_KINDS
# Set valid intervals.
INTERVALS = ['years', 'months', 'weeks', 'days', 'hours', 'minutes']
+TRANSFER_KINDS = stats_client.TRANSFER_KINDS
+VALID_STATS = SMS_KINDS + CALL_KINDS + GPRS_KINDS + TIMESERIES_STAT_KEYS + \
+ TRANSFER_KINDS + SUBSCRIBER_KINDS + WATERFALL_KINDS + \
+ BTS_STATUS + DENOMINATION_KINDS + NON_LOADER_KINDS
# Set valid aggregation types.
AGGREGATIONS = ['count', 'duration', 'up_byte_count', 'down_byte_count',
- 'average_value']
-
+ 'average_value', 'transaction_sum', 'transcation_count',
+ 'duration_minute']
+REPORT_VIEWS = ['summary', 'list']
# Any requested start time earlier than this date will be set to this date.
# This comes from a bug where UEs were generated at an astounding rate in
@@ -52,6 +71,9 @@ def parse_query_params(params):
'stat-types': ['sms'],
'level-id': -1,
'aggregation': 'count',
+ 'report-view': 'list',
+ 'extras': [],
+ 'topup-percent': None,
}
# Override defaults with any query params that have been set, if the
# query params are valid.
@@ -74,10 +96,20 @@ def parse_query_params(params):
# If nothing validated, we just leave the stat-types as the default.
if validated_types:
parsed_params['stat-types'] = validated_types
+ # Check if stat-type is dynamic currently for denominations
+ if params.has_key('dynamic-stat') and bool(params['dynamic-stat']):
+ parsed_params['stat-types'] = stat_types
+ # For filtering top topups as per percentage
+ if params.has_key('topup-percent'):
+ parsed_params['topup-percent'] = params['topup-percent']
if 'level-id' in params:
parsed_params['level-id'] = int(params['level-id'])
if 'aggregation' in params and params['aggregation'] in AGGREGATIONS:
parsed_params['aggregation'] = params['aggregation']
+ if 'extras' in params:
+ parsed_params['extras'] = params['extras'].split(',')
+ if 'report-view' in params and params['report-view'] in REPORT_VIEWS:
+ parsed_params['report-view'] = params['report-view']
return parsed_params
@@ -125,7 +157,7 @@ def get(self, request, infrastructure_level):
data = {
'results': [],
}
- for stat_type in params['stat-types']:
+ for index, stat_type in enumerate(params['stat-types']):
# Setup the appropriate stats client, SMS, call or GPRS.
if stat_type in SMS_KINDS:
client_type = stats_client.SMSStatsClient
@@ -135,6 +167,18 @@ def get(self, request, infrastructure_level):
client_type = stats_client.GPRSStatsClient
elif stat_type in TIMESERIES_STAT_KEYS:
client_type = stats_client.TimeseriesStatsClient
+ elif stat_type in SUBSCRIBER_KINDS:
+ client_type = stats_client.SubscriberStatsClient
+ elif stat_type in TRANSFER_KINDS:
+ client_type = stats_client.TransferStatsClient
+ elif stat_type in BTS_STATUS:
+ client_type = stats_client.BTSStatsClient
+ elif stat_type in WATERFALL_KINDS:
+ client_type = stats_client.WaterfallStatsClient
+ elif stat_type in NON_LOADER_KINDS:
+ client_type = stats_client.NonLoaderStatsClient
+ else:
+ client_type = stats_client.TopUpStatsClient
# Instantiate the client at an infrastructure level.
if infrastructure_level == 'global':
client = client_type('global')
@@ -142,6 +186,10 @@ def get(self, request, infrastructure_level):
client = client_type('network', params['level-id'])
elif infrastructure_level == 'tower':
client = client_type('tower', params['level-id'])
+ try:
+ extra_param = params['extras'][index]
+ except IndexError:
+ extra_param = None
# Get timeseries results and append it to data.
results = client.timeseries(
stat_type,
@@ -149,10 +197,24 @@ def get(self, request, infrastructure_level):
start_time_epoch=params['start-time-epoch'],
end_time_epoch=params['end-time-epoch'],
aggregation=params['aggregation'],
+ report_view=params['report-view'],
+ extras=extra_param,
+ topup_percent=params['topup-percent']
)
+ if stat_type in TRANSFER_KINDS:
+ table_view = client.timeseries(
+ stat_type,
+ start_time_epoch=params['start-time-epoch'],
+ end_time_epoch=params['end-time-epoch'],
+ aggregation=params['aggregation'],
+ report_view ="table_view"
+ )
+ else:
+ table_view = {}
data['results'].append({
"key": stat_type,
- "values": results
+ "values": results,
+ "retailer_table_data":table_view
})
# Convert params.stat_types back to CSV and echo back the request.
diff --git a/cloud/endagaweb/templates/dashboard/layout.html b/cloud/endagaweb/templates/dashboard/layout.html
index c4567a7f..5b7ed374 100644
--- a/cloud/endagaweb/templates/dashboard/layout.html
+++ b/cloud/endagaweb/templates/dashboard/layout.html
@@ -23,23 +23,19 @@
-
+
{% block pagestyle %}
{% endblock %}
- {% if user_profile.user.is_staff %}
+ {% if user_profile.user.is_superuser %}