Ssmith has uploaded a new change for review. https://gerrit.wikimedia.org/r/196614
Change subject: Fraud gauge widget refactor ...................................................................... Fraud gauge widget refactor Change-Id: I97821c63e25ab10bc08ea3cb5ea5de0204eb0db0 --- M src/app/widgetBase.js M src/components/widgets/fraud-gauge/fraud-gauge.html M src/components/widgets/fraud-gauge/fraud-gauge.js M src/css/style.css 4 files changed, 269 insertions(+), 273 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/wikimedia/fundraising/dash refs/changes/14/196614/1 diff --git a/src/app/widgetBase.js b/src/app/widgetBase.js index ba71d70..30a52c1 100644 --- a/src/app/widgetBase.js +++ b/src/app/widgetBase.js @@ -26,6 +26,7 @@ self.chartWidth = ko.observable('900'); self.chartHeight = ko.observable('550'); self.chartLoaded = ko.observable(false); + self.chartModified = ko.observable(false); self.getChartData = function( qs ){ self.dataLoading(true); @@ -50,6 +51,7 @@ }), success: function( data ) { self.chartSaved(true); + self.chartModified(false); } }); } else { @@ -64,6 +66,7 @@ success: function( data ) { self.instanceID = data.id; self.chartSaved(true); + self.chartModified(false); } }); } diff --git a/src/components/widgets/fraud-gauge/fraud-gauge.html b/src/components/widgets/fraud-gauge/fraud-gauge.html index e3b9fed..633a953 100644 --- a/src/components/widgets/fraud-gauge/fraud-gauge.html +++ b/src/components/widgets/fraud-gauge/fraud-gauge.html @@ -1,11 +1,13 @@ <div class="row"> - <div data-bind="attr: {class: columnSize}"> + <div class="col-md-6"> <div class="panel panel-purple" id="fraudGaugeWidget"> <div class="panel-heading"> <span data-bind='text: title'></span> <div class="btn-group btn-group-xs pull-right"> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="modal" data-target="#sqlModal"><i class="fa fa-database"></i></button> <button type="button" class="btn btn-default" data-toggle="modal" data-target="#modifyModal"><i class="fa fa-edit"></i></button> + <button type="button" class="btn btn-danger" data-bind="visible: chartModified, click: saveWidgetConfig"><i class="fa fa-save"></i></button> + <button type="button" class="btn btn-success" data-bind="visible: chartSaved && !chartModified()"><i class="fa fa-save"></i> Saved</button> </div> </div> <div class="panel-body"> @@ -14,14 +16,14 @@ <div class="gauge"> <div class="row"> - <h3 class="text-center" data-bind="visible: gaugeIsSetUp(), text: selectedTimePeriod"></h3> + <h3 class="text-center" data-bind="visible: chartSaved, text: selectedTimePeriod"></h3> </div> - <div class="row" id="specifiedFraudGauge" data-bind="visible: gaugeIsSetUp"> + <div class="row" id="specifiedFraudGauge" data-bind="visible: chartSaved"> <div id="FraudRiskScoreGauge"></div> </div> - <div class="row" id="unspecifiedFraudGauge" data-bind="visible: !gaugeIsSetUp()"> + <div class="row" id="unspecifiedFraudGauge" data-bind="visible: !chartSaved()"> <div class="col-md-12"> <div class="setupAsk"> <h3>This widget hasn't been set up yet.</h3> @@ -30,7 +32,7 @@ </div> </div> - <div class="row" id="fraudGaugeMeta" data-bind="visible: gaugeIsSetUp()"> + <div class="row" id="fraudGaugeMeta" data-bind="visible: chartSaved"> <p class="text-center"><span data-bind="visible: filtersSelected()">By: </span><span class="label label-info" data-bind="text: selectedFilters"></span></p> </div> @@ -46,14 +48,14 @@ <form role="form" class="fraudForm"> <div class="form-group"> <div class="well"> - <label for="fraudPercentSlider">Fraud Percent Ranges</label> + <label for="fraudPercentRanges">Fraud Percent Ranges</label> <p>set up the colors as they'll appear in the gauge.</p> - <canvas id="fraudPercentSlider" width="200" height="120"></canvas> + <canvas id="fraudPercentRanges" width="200" height="120"></canvas> <div class="row"> <div class="col-md-4 col-sm-4 col-lg-4"> <label>low fraud score: up to</label> <div class="input-group"> - <input type="text" class="form-control" data-bind="textInput: greenHighRange, event: { mouseout: renderGaugeChart }"> + <input type="text" class="form-control" data-bind="textInput: greenHighRange, event: { mouseout: renderPercentRangeChart }"> <span class="input-group-btn"> <button class="btn btn-success percentBtn" type="button"><i class="fa fa-refresh"></i></button> </span> @@ -70,7 +72,7 @@ <div class="col-md-4 col-sm-4 col-lg-4"> <label>high fraud score: from</label> <div class="input-group"> - <input type="text" class="form-control" data-bind="textInput: redLowRange, event: { mouseout: renderGaugeChart }"> + <input type="text" class="form-control" data-bind="textInput: redLowRange, event: { mouseout: renderPercentRangeChart }"> <span class="input-group-btn"> <button class="btn btn-danger percentBtn" type="button"><i class="fa fa-refresh"></i></button> </span> diff --git a/src/components/widgets/fraud-gauge/fraud-gauge.js b/src/components/widgets/fraud-gauge/fraud-gauge.js index 8fca0bd..9b3fccf 100644 --- a/src/components/widgets/fraud-gauge/fraud-gauge.js +++ b/src/components/widgets/fraud-gauge/fraud-gauge.js @@ -1,308 +1,299 @@ define([ - 'knockout', - 'text!components/widgets/fraud-gauge/fraud-gauge.html', - 'c3', - 'chartjs' - ], -function( ko, template, c3, Chart ){ - //extend the chart so we can flip the circle - Chart.types.Doughnut.extend({ - addData: function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - this.segments.splice(index, 0, new this.SegmentArc({ - value : segment.value, - outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, - innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, - fillColor : segment.color, - highlightColor : segment.highlight || segment.color, - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - startAngle : Math.PI * 2.5, - circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), - label : segment.label - })); - if (!silent){ - this.reflow(); - this.update(); - } - }}); + 'knockout', + 'text!components/widgets/fraud-gauge/fraud-gauge.html', + 'c3', + 'chartjs', + 'WidgetBase' + ], +function( ko, template, c3, Chart, WidgetBase ){ - function FraudGaugeViewModel( params ){ + //extend the chart so we can flip the circle + Chart.types.Doughnut.extend({ + addData: function(segment, atIndex, silent){ + var index = atIndex || this.segments.length; + this.segments.splice(index, 0, new this.SegmentArc({ + value : segment.value, + outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, + innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, + fillColor : segment.color, + highlightColor : segment.highlight || segment.color, + showStroke : this.options.segmentShowStroke, + strokeWidth : this.options.segmentStrokeWidth, + strokeColor : this.options.segmentStrokeColor, + startAngle : Math.PI * 2.5, + circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + label : segment.label + })); - var self = this; - self.filters = ko.observableArray(); - self.title = ko.observable(params.title); - console.log('fraud widget params: ', params); - self.columnSize = 'col-md-' + params.configuration.width + ' fraudGauge'; + if (!silent){ + this.reflow(); + this.update(); + } + } + }); - $.get( 'metadata/fraud-gauge', function(reqData){ - self.data = reqData; - //broken down data from above - self.filters($.map(self.data.filters, function(val, i){return [val];})); - self.filterNames = ko.computed( function(){ - var names = []; - $.each(self.filters(), function(el, i){ - names.push(i.display); - }); - return names; - }); - }); + function FraudGaugeViewModel( params ){ - self.selectedTimePeriod = ko.observable('Last 15 Minutes'); - self.selectedFilters = ko.observableArray([]); - self.selectedSubFilters = ko.observableArray([]); - self.queryRequest = []; - self.gaugeValue = ko.observable(0); - self.filtersSelected = ko.observable(false); - self.gaugeIsSetUp = ko.observable(false); - self.queryStringSQL = ko.observable('This widget hasn\'t been set up yet!'); + WidgetBase.call( this, params ); + var self = this, + wasSaved = self.chartSaved(); + self.filters = ko.observableArray(); + self.title = ko.observable(params.title); + self.queryString = ''; + self.columnSize = 'col-md-' + params.configuration.width + ' fraudGauge'; + self.selectedTimePeriod = ko.observable('Last 15 Minutes'); + self.selectedFilters = ko.observableArray([]); + self.selectedSubFilters = ko.observableArray([]); + self.queryRequest = []; + self.gaugeValue = ko.observable(0); + self.filtersSelected = ko.observable(false); + self.queryStringSQL = ko.observable('This widget hasn\'t been set up yet!'); + self.greenHighRange = ko.observable(17); + self.redLowRange = ko.observable(68); - //default range slider settings - self.greenHighRange = ko.observable(17); - self.redLowRange = ko.observable(68); + self.populateChoices = function(){ + return $.get( 'metadata/fraud-gauge', function(reqData){ + self.data = reqData; - self.renderGaugeChart = function(){ + self.filters($.map(self.data.filters, function(val, i){return [val];})); + self.filterNames = ko.computed( function(){ + var names = []; - //color selection inside modal - var canvas = $('#fraudPercentSlider')[0]; - var ctx = canvas.getContext('2d'); + $.each(self.filters(), function(el, i){ + names.push(i.display); + }); - var placeholder = document.createElement('canvas'); - placeholder.width = 200; - placeholder.height = placeholder.width; - var placeholderctx = placeholder.getContext('2d'); + return names; + }); + }); + }; - var ddata = [{ - value: 90, - color: '#000000' - },{ - value: 1.8 * (self.greenHighRange()), - color: '#4cae4c' - },{ - value: 1.8 * (self.redLowRange() - self.greenHighRange()), - color: '#eea236' - }, { - value: 1.8 * (100 - self.redLowRange()), - color: '#c9302c' - },{ - value: 90, - color: '#000000' - }]; + self.renderPercentRangeChart = function(){ - //draw chart - self.gaugeChart = new Chart(placeholderctx).Doughnut(ddata, { - animation: false, - segmentShowStroke: false, + var canvas = $('#fraudPercentRanges')[0], + ctx = canvas.getContext('2d'); - onAnimationComplete: function() { + var placeholder = document.createElement('canvas'); + placeholder.width = 200; + placeholder.height = placeholder.width; + var placeholderctx = placeholder.getContext('2d'); - var cropHeight = Math.round(placeholder.height/2); + var ddata = [{ + value: 90, + color: '#000000' + },{ + value: 1.8 * (self.greenHighRange()), + color: '#4cae4c' + },{ + value: 1.8 * (self.redLowRange() - self.greenHighRange()), + color: '#eea236' + }, { + value: 1.8 * (100 - self.redLowRange()), + color: '#c9302c' + },{ + value: 90, + color: '#000000' + }]; - ctx.clearRect(0,0,canvas.width,canvas.height); + self.gaugeChart = new Chart( placeholderctx ).Doughnut( ddata, { + animation: false, + segmentShowStroke: false, + onAnimationComplete: function() { + var cropHeight = Math.round(placeholder.height/2); + ctx.clearRect(0,0,canvas.width,canvas.height); + ctx.drawImage( + placeholder, + 0, + 0, + placeholder.width, + cropHeight, + 0, + 0, + placeholder.width, + cropHeight + ); + } + }); + }(); - ctx.drawImage( - placeholder, - 0, - 0, - placeholder.width, - cropHeight, - 0, - 0, - placeholder.width, - cropHeight - ); + self.makeChart = function(){ + self.gauge = c3.generate({ + bindto: '#FraudRiskScoreGauge', + size: { + height: 300, + width: 390 + }, + data: { + columns: [ + ['failure', self.gaugeValue()] + ], + type: 'gauge', + onclick: function (d, i) { console.log('onclick', d, i); }, //TODO: make these better + onmouseover: function (d, i) { console.log('onmouseover', d, i); }, + onmouseout: function (d, i) { console.log('onmouseout', d, i); } + }, + gauge: { + min: 0, + max: 100, + units: 'failure rate' + }, + color: { + pattern: ['#FF0000', '#F97600', '#F6C600', '#60B044'], // the three color levels for the percentage values. + threshold: { + values: [ 0, self.greenHighRange, self.redLowRange, 100] + } + } + }); + }; - } - }); + self.validateSubmission = function( times, filters ){ + var validation = { + validated: '', + errors: [] + }; - }; + if( !times ){ + validation.errors.push('You must submit a valid time.'); + validation.validated = false; + } else { + validation.validated = true; + } - self.renderGaugeChart(); + return validation; + }; - self.validateSubmission = function( times, filters ){ + self.convertToQueryString = function( userChoices ){ + var qs = '', + ds = '', + timePresets = [ 'Last 15 Minutes', + 'Last Hour', + 'Last 24 Hours', + 'Last 5 Minutes']; - var validation = { - validated: '', - errors: [] - }; + var filterObj = {}; + var haveMultipleSubfilters = []; + $.each( userChoices.selectedSubFilters, function(el, subfilter){ + var filter = subfilter.substr(0, subfilter.indexOf(' ')); - //there must be a chosen timeframe - if(!times){ - validation.errors.push('You must submit a valid time.'); - validation.validated = false; - } else { - validation.validated = true; - } + if(!filterObj[filter]){ + filterObj[filter] = subfilter; + } else { + filterObj[filter] += ' or ' + subfilter; + haveMultipleSubfilters.push(filter); + } + }); - return validation; - }; + $.each( filterObj, function(el, s){ + if( haveMultipleSubfilters.indexOf(el) > -1){ + qs += '(' + filterObj[el] + ')'; + } else { + qs += filterObj[el]; + } + qs += ' and '; + }); - self.convertToQuery = function( userChoices ){ + var currentDate = new Date(); + switch( userChoices.timespan[0] ){ + case timePresets[0]: + var lfm = new Date(currentDate.getTime() - (15 * 60 * 1000)); + ds += 'DT gt \'' + lfm.toISOString() + '\''; + break; + case timePresets[1]: + var lh = new Date(currentDate.getTime() - (60 * 60 * 1000)); + ds += 'DT gt \'' + lh.toISOString() + '\''; + break; + case timePresets[2]: + var ltfh = new Date(currentDate.getTime() - (24 * 60 * 60 * 1000)); + ds += 'DT gt \'' + ltfh.toISOString() + '\''; + break; + case timePresets[3]: + var lfvm = new Date(currentDate.getTime() - (5 * 60 * 1000)); + ds += 'DT gt \'' + lfvm.toISOString() + '\''; + break; + default: + var lfm2 = new Date(currentDate.getTime() - (15 * 60 * 1000)); + ds += 'DT gt \'' + lfm2.toISOString() + '\''; + break; - var qs = '', - ds = '', - timePresets = [ 'Last 15 Minutes', - 'Last Hour', - 'Last 24 Hours', - 'Last 5 Minutes']; + } - //match subfilters to filters - //TODO: this is terrible and needs to be refactored when this piece gets modularized. - var filterObj = {}; - var haveMultipleSubfilters = []; - $.each( userChoices.selectedSubFilters, function(el, subfilter){ - var filter = subfilter.substr(0, subfilter.indexOf(' ')); + var postQS = ''; + if(qs.length > 0){ + postQS = qs + ds; + } else { + postQS = ds; + } - if(!filterObj[filter]){ - filterObj[filter] = subfilter; - } else { - filterObj[filter] += ' or ' + subfilter; - haveMultipleSubfilters.push(filter); - } - }); + return "$filter=" + postQS; + }; - $.each( filterObj, function(el, s){ - if( haveMultipleSubfilters.indexOf(el) > -1){ - qs += '(' + filterObj[el] + ')'; - } else { - qs += filterObj[el]; - } - qs += ' and '; - }); + self.showSubfilters = function( stuff ){ + $('#'+stuff).toggleClass('hide'); + }; - //convert time constraints - var currentDate = new Date(); - switch( userChoices.timespan[0] ){ - case timePresets[0]: - var lfm = new Date(currentDate.getTime() - (15 * 60 * 1000)); - ds += 'DT gt \'' + lfm.toISOString() + '\''; - break; - case timePresets[1]: - var lh = new Date(currentDate.getTime() - (60 * 60 * 1000)); - ds += 'DT gt \'' + lh.toISOString() + '\''; - break; - case timePresets[2]: - var ltfh = new Date(currentDate.getTime() - (24 * 60 * 60 * 1000)); - ds += 'DT gt \'' + ltfh.toISOString() + '\''; - break; - case timePresets[3]: - var lfvm = new Date(currentDate.getTime() - (5 * 60 * 1000)); - ds += 'DT gt \'' + lfvm.toISOString() + '\''; - break; - default: - var lfm2 = new Date(currentDate.getTime() - (15 * 60 * 1000)); - ds += 'DT gt \'' + lfm2.toISOString() + '\''; - break; + self.resetGaugeSettings = function(){ + self.greenHighRange(33); + self.redLowRange(66); + self.renderPercentRangeChart(); - } + $('#timePeriodDropdown option:eq(0)').prop('selected', true); + $('.subfilterSubnav').addClass('hide'); + $('input:checkbox').removeAttr('checked'); + }; - //if there's already something in the qs, precede new string with 'and' - var postQS = ''; - if(qs.length > 0){ - postQS = qs + ds; - } else { - postQS = ds; - } + self.submitGaugeModifications = function(){ + self.chartModified(true); - return postQS; - }; + var validation = self.validateSubmission( self.selectedTimePeriod(), self.selectedFilters() ); + if( !validation.validated ){ - self.showSubfilters = function(stuff){ - $('#'+stuff).toggleClass('hide'); - }; + $('#fraudSubmissionErrors').html('<p class="text-danger">you have errors in your submission:</p><ul></ul>' ).addClass('show'); + $.each( validation.errors, function(el, i){ + $('#fraudSubmissionErrors ul').append('<li>' + i + '</li>'); + }); - self.resetGaugeSettings = function(){ + } else{ - //reset gauge settings to defaults - self.greenHighRange(33); - self.redLowRange(66); - self.renderGaugeChart(); + //gauge time period + self.queryRequest.timespan = self.selectedTimePeriod(); - //reset datepicker - $('#timePeriodDropdown option:eq(0)').prop('selected', true); + //gauge filters + self.queryRequest.selectedFilters = self.selectedFilters(); + if(self.selectedFilters().length > 0){ + self.filtersSelected(true); + } - //reset filters - $('.subfilterSubnav').addClass('hide'); - $('input:checkbox').removeAttr('checked'); + //gauge subfilters + self.queryRequest.selectedSubFilters = self.selectedSubFilters().sort(); + self.queryString = self.convertToQueryString(self.queryRequest); - }; + var chartDataCall = self.getChartData( self.queryString ); + $.when( chartDataCall ).then( function( dataget ){ + self.gaugeValue(parseFloat(dataget.results[0].fraud_percent).toFixed(2) ); + self.queryStringSQL(dataget.sqlQuery); + self.makeChart(); + }); + } + }; - self.submitGaugeModifications = function(){ + self.populateChoices().then(function() { + self.preDataLoading(false); - //validate values first. - var validation = self.validateSubmission( self.selectedTimePeriod(), self.selectedFilters() ); - if( !validation.validated ){ + if ( wasSaved ) { + // restore choices and show the chart + self.selectedTimePeriod(); + self.selectedFilters(); - $('#fraudSubmissionErrors').html('<p class="text-danger">you have errors in your submission:</p><ul></ul>' ).addClass('show'); - $.each( validation.errors, function(el, i){ - $('#fraudSubmissionErrors ul').append('<li>' + i + '</li>'); - }); + self.chartModified(false); + self.chartSaved( true ); + self.submitGaugeModifications(); + } + }); - } else{ + return this; + } - //gauge time period - self.queryRequest.timespan = self.selectedTimePeriod(); - - //gauge filters - self.queryRequest.selectedFilters = self.selectedFilters(); - if(self.selectedFilters().length > 0){ - self.filtersSelected(true); - } - - //gauge subfilters - self.queryRequest.selectedSubFilters = self.selectedSubFilters().sort(); - - //put it all into a real query - //this will be a function call - TODO: make parsing function - var queryString = self.convertToQuery(self.queryRequest); - - - //Todo: if this is already set up in configs, take that data. - //otherwise do this. - $.get( '/data/fraud?' + $.param({ '$filter': queryString }).replace( - /\+/g, '%20' ), function ( dataget ) { - self.gaugeIsSetUp(true); - self.gaugeValue(parseFloat(dataget.results[0].fraud_percent).toFixed(2) ); - self.queryStringSQL(dataget.sqlQuery); - - self.gauge = c3.generate({ - bindto: '#FraudRiskScoreGauge', - size: { - height: 300, - width: 390 - }, - data: { - columns: [ - ['failure', self.gaugeValue()] - ], - type: 'gauge', - onclick: function (d, i) { console.log('onclick', d, i); }, //TODO: make these better - onmouseover: function (d, i) { console.log('onmouseover', d, i); }, - onmouseout: function (d, i) { console.log('onmouseout', d, i); } - }, - gauge: { - min: 0, - max: 100, - units: 'failure rate' - }, - color: { - pattern: ['#FF0000', '#F97600', '#F6C600', '#60B044'], // the three color levels for the percentage values. - threshold: { - values: [ 0, self.greenHighRange, self.redLowRange, 100] - } - } - }); - }); - } - - }; - - - } - - return { viewModel: FraudGaugeViewModel, template: template }; - + return { viewModel: FraudGaugeViewModel, template: template }; }); diff --git a/src/css/style.css b/src/css/style.css index f0a0982..cebde9f 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -299,7 +299,7 @@ margin: 0 auto; } -#fraudPercentSlider { +#fraudPercentRanges { margin: 0 auto; display: inherit; } -- To view, visit https://gerrit.wikimedia.org/r/196614 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I97821c63e25ab10bc08ea3cb5ea5de0204eb0db0 Gerrit-PatchSet: 1 Gerrit-Project: wikimedia/fundraising/dash Gerrit-Branch: master Gerrit-Owner: Ssmith <ssm...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits