mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-05 16:24:21 +01:00
650 lines
22 KiB
CoffeeScript
650 lines
22 KiB
CoffeeScript
'use strict'
|
|
|
|
Application.Controllers.controller "GraphsController", ["$scope", "$state", "$rootScope", 'es', 'Statistics', '_t'
|
|
, ($scope, $state, $rootScope, es, Statistics, _t) ->
|
|
|
|
|
|
|
|
### PRIVATE STATIC CONSTANTS ###
|
|
|
|
## height of the HTML/SVG charts elements in pixels
|
|
CHART_HEIGHT = 500
|
|
|
|
## Label of the charts' horizontal axes
|
|
X_AXIS_LABEL = _t('date')
|
|
|
|
## Label of the charts' vertical axes
|
|
Y_AXIS_LABEL = _t('number')
|
|
|
|
## Colors for the line charts. Each new line uses the next color in this array
|
|
CHART_COLORS = ['#b35a94', '#1c5794', '#00b49e', '#6fac48', '#ebcf4a', '#fd7e33', '#ca3436', '#a26e3a']
|
|
|
|
|
|
|
|
### PUBLIC SCOPE ###
|
|
|
|
## ui-view transitions optimization: if true, the charts will never be refreshed
|
|
$scope.preventRefresh = false
|
|
|
|
## statistics structure in elasticSearch
|
|
$scope.statistics = []
|
|
|
|
## statistics data recovered from elasticSearch
|
|
$scope.data = null
|
|
|
|
## default interval: one day
|
|
$scope.display =
|
|
interval: 'week'
|
|
|
|
## active tab will be set here
|
|
$scope.selectedIndex = null
|
|
|
|
## for palmares graphs, filters values are stored here
|
|
$scope.ranking =
|
|
sortCriterion: 'ca'
|
|
groupCriterion: 'subType'
|
|
|
|
## default: we do not open the datepicker menu
|
|
$scope.datePicker =
|
|
show: false
|
|
|
|
## datePicker parameters for interval beginning
|
|
$scope.datePickerStart =
|
|
format: Fablab.uibDateFormat
|
|
opened: false # default: datePicker is not shown
|
|
minDate: null
|
|
maxDate: moment().subtract(1, 'day').toDate()
|
|
selected: moment().utc().subtract(1, 'months').subtract(1, 'day').startOf('day').toDate()
|
|
options:
|
|
startingDay: Fablab.weekStartingDay
|
|
|
|
## datePicker parameters for interval ending
|
|
$scope.datePickerEnd =
|
|
format: Fablab.uibDateFormat
|
|
opened: false # default: datePicker is not shown
|
|
minDate: null
|
|
maxDate: moment().subtract(1, 'day').toDate()
|
|
selected: moment().subtract(1, 'day').endOf('day').toDate()
|
|
options:
|
|
startingDay: Fablab.weekStartingDay
|
|
|
|
|
|
|
|
##
|
|
# Callback to open the datepicker (interval start)
|
|
# @param {Object} jQuery event object
|
|
##
|
|
$scope.toggleStartDatePicker = ($event) ->
|
|
toggleDatePicker($event, $scope.datePickerStart)
|
|
|
|
|
|
|
|
##
|
|
# Callback to open the datepicker (interval end)
|
|
# @param {Object} jQuery event object
|
|
##
|
|
$scope.toggleEndDatePicker = ($event) ->
|
|
toggleDatePicker($event, $scope.datePickerEnd)
|
|
|
|
|
|
|
|
##
|
|
# Callback called when the active tab is changed.
|
|
# Recover the current tab and store its value in $scope.selectedIndex
|
|
# @param tab {Object} elasticsearch statistic structure
|
|
##
|
|
$scope.setActiveTab = (tab) ->
|
|
$scope.selectedIndex = tab
|
|
$scope.ranking.groupCriterion = 'subType'
|
|
if tab.ca
|
|
$scope.ranking.sortCriterion = 'ca'
|
|
else
|
|
$scope.ranking.sortCriterion = tab.types[0].key
|
|
refreshChart()
|
|
|
|
|
|
|
|
##
|
|
# Callback to close the date-picking popup and refresh the results
|
|
##
|
|
$scope.validateDateChange = ->
|
|
$scope.datePicker.show = false
|
|
refreshChart()
|
|
|
|
|
|
|
|
### PRIVATE SCOPE ###
|
|
|
|
##
|
|
# Kind of constructor: these actions will be realized first when the controller is loaded
|
|
##
|
|
initialize = ->
|
|
Statistics.query (stats) ->
|
|
$scope.statistics = stats
|
|
# watch the interval changes to refresh the graph
|
|
$scope.$watch (scope) ->
|
|
return scope.display.interval
|
|
, (newValue, oldValue) ->
|
|
refreshChart()
|
|
$scope.$watch (scope) ->
|
|
return scope.ranking.sortCriterion
|
|
, (newValue, oldValue) ->
|
|
refreshChart()
|
|
$scope.$watch (scope) ->
|
|
return scope.ranking.groupCriterion
|
|
, (newValue, oldValue) ->
|
|
refreshChart()
|
|
refreshChart()
|
|
|
|
# workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected
|
|
# which will cause every tabs to reload, one by one, when the view is closed
|
|
$rootScope.$on '$stateChangeStart', (event, toState, toParams, fromState, fromParams) ->
|
|
if fromState.name == 'app.admin.stats_graphs' and Object.keys(fromParams).length == 0
|
|
$scope.preventRefresh = true
|
|
|
|
|
|
|
|
##
|
|
# Generic function to toggle a bootstrap datePicker
|
|
# @param $event {Object} jQuery event object
|
|
# @param datePicker {Object} settings object of the concerned datepicker. Must have an 'opened' property
|
|
##
|
|
toggleDatePicker = ($event, datePicker) ->
|
|
$event.preventDefault()
|
|
$event.stopPropagation()
|
|
datePicker.opened = !datePicker.opened
|
|
|
|
|
|
|
|
##
|
|
# Query elasticSearch according to the current parameters and update the chart
|
|
##
|
|
refreshChart = ->
|
|
if $scope.selectedIndex and !$scope.preventRefresh
|
|
query $scope.selectedIndex, (aggregations, error)->
|
|
if error
|
|
console.error(error)
|
|
else
|
|
if $scope.selectedIndex.graph.chart_type != 'discreteBarChart'
|
|
$scope.data = formatAggregations(aggregations)
|
|
angular.forEach $scope.data, (datum, key) ->
|
|
updateChart($scope.selectedIndex.graph.chart_type, datum, key)
|
|
else
|
|
$scope.data = formatRankingAggregations(aggregations, $scope.selectedIndex.graph.limit, $scope.ranking.groupCriterion)
|
|
updateChart($scope.selectedIndex.graph.chart_type, $scope.data.ranking, $scope.selectedIndex.es_type_key)
|
|
|
|
|
|
|
|
##
|
|
# Callback used in NVD3 to print timestamps as literal dates on the X axis
|
|
##
|
|
xAxisTickFormatFunction = (d, x, y) ->
|
|
### WARNING !! These tests (typeof/instanceof) may become broken on nvd3 update ###
|
|
if $scope.display.interval == 'day'
|
|
if typeof d == 'number' or d instanceof Date
|
|
d3.time.format(Fablab.d3DateFormat) moment(d).toDate()
|
|
else # typeof d == 'string'
|
|
d
|
|
else if $scope.display.interval == 'week'
|
|
if typeof x == 'number' or d instanceof Date
|
|
d3.time.format(_t('week_short')+' %U') moment(d).toDate()
|
|
else if typeof d == 'number'
|
|
_t('week_of_START_to_END', {START:moment(d).format('L'), END:moment(d).add(6, 'days').format('L')})
|
|
else # typeof d == 'string'
|
|
d
|
|
else if $scope.display.interval == 'month'
|
|
if typeof d == 'number'
|
|
label = moment(d).format('MMMM YYYY')
|
|
label.substr(0,1).toUpperCase()+label.substr(1).toLowerCase()
|
|
else # typeof d == 'string'
|
|
d
|
|
|
|
|
|
|
|
##
|
|
# Format aggregations as retuned by elasticSearch to an understandable format for NVD3
|
|
# @param aggs {Object} as returned by elasticsearch
|
|
##
|
|
formatAggregations = (aggs) ->
|
|
format = {}
|
|
|
|
angular.forEach aggs, (type, type_key) -> # go through aggs[$TYPE] where $TYPE = month|year|hour|booking|...
|
|
format[type_key] = []
|
|
if type.subgroups
|
|
angular.forEach type.subgroups.buckets, (subgroup) -> # go through aggs.$TYPE.subgroups.buckets where each bucket represent a $SUBTYPE
|
|
angular.forEach $scope.selectedIndex.types, (cur_type) -> # in the mean time, go through the types of the current index (active tab) ...
|
|
if cur_type.key == type_key # ... looking for the type matching $TYPE
|
|
for it_st in [0.. cur_type.subtypes.length-1] by 1 # when we've found it, iterate over its subtypes ...
|
|
cur_subtype = cur_type.subtypes[it_st]
|
|
if subgroup.key == cur_subtype.key # ... which match $SUBTYPE
|
|
# then we construct NVD3 dataSource according to these informations
|
|
dataSource =
|
|
values: []
|
|
key: cur_subtype.label
|
|
total : 0
|
|
color: CHART_COLORS[it_st]
|
|
area: true
|
|
# finally, we iterate over 'intervals' buckets witch contains
|
|
# per date aggregations for our current dataSource
|
|
angular.forEach subgroup.intervals.buckets, (interval) ->
|
|
dataSource.values.push
|
|
x: interval.key
|
|
y: interval.total.value
|
|
dataSource.total += parseInt(interval.total.value)
|
|
dataSource.key += ' (' + dataSource.total + ')'
|
|
format[type_key].push dataSource
|
|
format
|
|
|
|
|
|
|
|
##
|
|
# Format aggregations for ranking charts to an understandable format for NVD3
|
|
# @param aggs {Object} as returned by elasticsearch
|
|
# @param limit {number} limit the number of stats in the bar chart
|
|
# @param typeKey {String} field name witch results are grouped by
|
|
##
|
|
formatRankingAggregations = (aggs, limit, typeKey) ->
|
|
format =
|
|
ranking: []
|
|
|
|
it = 0
|
|
while (it < aggs.subgroups.buckets.length)
|
|
bucket = aggs.subgroups.buckets[it]
|
|
dataSource =
|
|
values: []
|
|
key: getRankingLabel(bucket.key, typeKey)
|
|
color: CHART_COLORS[it]
|
|
area: true
|
|
dataSource.values.push
|
|
x: getRankingLabel(bucket.key, typeKey)
|
|
y: bucket.total.value
|
|
format.ranking.push(dataSource)
|
|
it++
|
|
getY = (object)->
|
|
object.values[0].y
|
|
format.ranking = stableSort(format.ranking, 'DESC', getY).slice(0, limit)
|
|
for i in [0..format.ranking.length] by 1
|
|
if typeof format.ranking[i] == 'undefined' then format.ranking.splice(i,1)
|
|
format
|
|
|
|
|
|
|
|
##
|
|
# For BarCharts, return the label for a given bar
|
|
# @param key {string} raw value of the label
|
|
# @param typeKey {string} name of the field the results are grouped by
|
|
##
|
|
getRankingLabel = (key, typeKey) ->
|
|
if $scope.selectedIndex
|
|
if (typeKey == 'subType')
|
|
for type in $scope.selectedIndex.types
|
|
for subtype in type.subtypes
|
|
if (subtype.key == key)
|
|
return subtype.label
|
|
else
|
|
for field in $scope.selectedIndex.additional_fields
|
|
if (field.key == typeKey)
|
|
switch field.data_type
|
|
when 'date' then return moment(key).format('LL')
|
|
when 'list' then return key.name
|
|
else return key
|
|
|
|
|
|
|
|
##
|
|
# Prepare the elasticSearch query for the stats matching the current controller's parameters
|
|
# @param index {{id:{number}, es_type_key:{string}, label:{string}, table:{boolean}, additional_fields:{Array},
|
|
# types:{Array}, graph:{Object}}} elasticSearch type in stats index to query
|
|
# @param callback {function} function be to run after results were retrieved,
|
|
# it will receive two parameters : results {Array}, error {String} (if any)
|
|
##
|
|
query = (index, callback) ->
|
|
# invalid callback handeling
|
|
if typeof(callback) != "function"
|
|
console.error('[graphsController::query] Error: invalid callback provided')
|
|
return
|
|
if !index
|
|
callback([], '[graphsController::query] Error: invalid index provided')
|
|
return
|
|
|
|
if index.graph.chart_type != 'discreteBarChart'
|
|
# list statistics types
|
|
stat_types = []
|
|
for t in index.types
|
|
if t.graph
|
|
stat_types.push(t.key)
|
|
|
|
# exception handeling
|
|
if stat_types.length == 0
|
|
callback([], "Error: Unable to retrieve any graphical statistic types in the provided index")
|
|
|
|
type_it = 0
|
|
results = {}
|
|
error = ''
|
|
recursiveCb = ->
|
|
if type_it < stat_types.length
|
|
queryElasticStats index.es_type_key, stat_types[type_it], (prevResults, prevError)->
|
|
if (prevError)
|
|
console.error('[graphsController::query] '+prevError)
|
|
error += '\n'+prevError
|
|
results[stat_types[type_it]] = prevResults
|
|
type_it++
|
|
recursiveCb()
|
|
else
|
|
callback(results)
|
|
recursiveCb()
|
|
else # palmares (ranking)
|
|
queryElasticRanking index.es_type_key, $scope.ranking.groupCriterion, $scope.ranking.sortCriterion, (results, error) ->
|
|
if (error)
|
|
callback([], error)
|
|
else
|
|
callback(results)
|
|
|
|
|
|
|
|
##
|
|
# Run the elasticSearch query to retreive the /stats/type aggregations
|
|
# @param esType {String} elasticSearch document type (subscription|machine|training|...)
|
|
# @param statType {String} statistics type (year|month|hour|booking|...)
|
|
# @param callback {function} function be to run after results were retrieved,
|
|
# it will receive two parameters : results {Array}, error {String} (if any)
|
|
##
|
|
queryElasticStats = (esType, statType, callback) ->
|
|
# handle invalid callback
|
|
if typeof(callback) != "function"
|
|
console.error('[graphsController::queryElasticStats] Error: invalid callback provided')
|
|
return
|
|
if !esType or !statType
|
|
callback([], '[graphsController::queryElasticStats] Error: invalid parameters provided')
|
|
|
|
# run query
|
|
es.search
|
|
"index": "stats"
|
|
"type": esType
|
|
"searchType": "count"
|
|
"body": buildElasticAggregationsQuery(statType, $scope.display.interval, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
|
|
, (error, response) ->
|
|
if (error)
|
|
callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
|
|
else
|
|
callback(response.aggregations)
|
|
|
|
|
|
|
|
##
|
|
# For ranking displays, run the elasticSearch query to retreive the /stats/type aggregations
|
|
# @param esType {string} elasticSearch document type (subscription|machine|training|...)
|
|
# @param groupKey {string} statistics subtype or custom field
|
|
# @param sortKey {string} statistics type or 'ca'
|
|
# @param callback {function} function be to run after results were retrieved,
|
|
# it will receive two parameters : results {Array}, error {String} (if any)
|
|
##
|
|
queryElasticRanking = (esType, groupKey, sortKey, callback) ->
|
|
# handle invalid callback
|
|
if typeof(callback) != "function"
|
|
console.error('[graphsController::queryElasticRanking] Error: invalid callback provided')
|
|
return
|
|
if !esType or !groupKey or !sortKey
|
|
callback([], '[graphsController::queryElasticRanking] Error: invalid parameters provided')
|
|
|
|
# run query
|
|
es.search
|
|
"index": "stats"
|
|
"type": esType
|
|
"searchType": "count"
|
|
"body": buildElasticAggregationsRankingQuery(groupKey, sortKey, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
|
|
, (error, response) ->
|
|
if (error)
|
|
callback([], "Error: something unexpected occurred during elasticSearch query: "+error)
|
|
else
|
|
callback(response.aggregations)
|
|
|
|
|
|
|
|
##
|
|
# Parse a final elastic results bucket and return a D3 compatible object
|
|
# @param bucket {{key_as_string:{String}, key:{Number}, doc_count:{Number}, total:{{value:{Number}}}}} interval bucket
|
|
##
|
|
parseElasticBucket = (bucket) ->
|
|
[ bucket.key, bucket.total.value ]
|
|
|
|
|
|
|
|
##
|
|
# Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
|
|
# currently defined for data aggegations.
|
|
# @param type {String} statistics type (visit|rdv|rating|ca|plan|account|search|...)
|
|
# @param interval {String} statistics interval (year|quarter|month|week|day|hour|minute|second)
|
|
# @param intervalBegin {moment} statitics interval beginning (moment.js type)
|
|
# @param intervalEnd {moment} statitics interval ending (moment.js type)
|
|
##
|
|
buildElasticAggregationsQuery = (type, interval, intervalBegin, intervalEnd) ->
|
|
q =
|
|
"query":
|
|
"bool":
|
|
"must": [
|
|
{
|
|
"match":
|
|
"type": type
|
|
}
|
|
{
|
|
"range":
|
|
"date":
|
|
"gte": intervalBegin.format()
|
|
"lte": intervalEnd.format()
|
|
}
|
|
]
|
|
"aggregations":
|
|
"subgroups":
|
|
"terms":
|
|
"field": "subType" #TODO allow aggregate by custom field
|
|
"aggregations":
|
|
"intervals":
|
|
"date_histogram":
|
|
"field": "date"
|
|
"interval": interval
|
|
"min_doc_count": 0
|
|
"extended_bounds":
|
|
"min": intervalBegin.valueOf()
|
|
"max": intervalEnd.valueOf()
|
|
"aggregations":
|
|
"total":
|
|
"sum":
|
|
"field": "stat"
|
|
|
|
# scale weeks on sunday as nvd3 supports only these weeks
|
|
if interval == 'week'
|
|
q.aggregations.subgroups.aggregations.intervals.date_histogram['post_offset'] = '-1d'
|
|
q.aggregations.subgroups.aggregations.intervals.date_histogram['pre_offset'] = '-1d'
|
|
# scale days to UTC time
|
|
else if interval == 'day'
|
|
offset = moment().utcOffset()
|
|
q.aggregations.subgroups.aggregations.intervals.date_histogram['post_offset'] = (-offset)+'m'
|
|
q
|
|
|
|
|
|
|
|
##
|
|
# Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
|
|
# currently defined for data aggegations.
|
|
# @param groupKey {String} statistics subtype or custom field
|
|
# @param sortKey {String} statistics type or 'ca'
|
|
# @param intervalBegin {moment} statitics interval beginning (moment.js type)
|
|
# @param intervalEnd {moment} statitics interval ending (moment.js type)
|
|
##
|
|
buildElasticAggregationsRankingQuery = (groupKey, sortKey, intervalBegin, intervalEnd) ->
|
|
q =
|
|
"query":
|
|
"bool":
|
|
"must": [
|
|
{
|
|
"range":
|
|
"date":
|
|
"gte": intervalBegin.format()
|
|
"lte": intervalEnd.format()
|
|
}
|
|
{
|
|
"term":
|
|
"type": "booking"
|
|
}
|
|
]
|
|
"aggregations":
|
|
"subgroups":
|
|
"terms":
|
|
"field": "subType"
|
|
"aggregations":
|
|
"total":
|
|
"sum":
|
|
"field": "stat"
|
|
|
|
# we group the results by the custom given key (eg. by event date)
|
|
q.aggregations.subgroups.terms =
|
|
field: groupKey
|
|
size: 0
|
|
|
|
# results must be sorted and limited later by angular
|
|
if sortKey != 'ca'
|
|
angular.forEach q.query.bool.must, (must) ->
|
|
if must.term
|
|
must.term.type = sortKey
|
|
else
|
|
q.aggregations.subgroups.aggregations.total.sum.field = sortKey
|
|
|
|
q
|
|
|
|
|
|
|
|
##
|
|
# Redraw the NDV3 chart using the provided data
|
|
# @param chart_type {String} stackedAreaChart|discreteBarChart|lineChart
|
|
# @param data {Array} array of NVD3 dataSources
|
|
# @param type {String} which chart to update (statistic type key)
|
|
##
|
|
updateChart = (chart_type, data, type) ->
|
|
|
|
id = "#chart-"+type+" svg"
|
|
|
|
# clean old charts
|
|
d3.selectAll(id+" > *").remove()
|
|
|
|
nv.addGraph ->
|
|
# no data or many dates, display line charts
|
|
if data.length == 0 or (data[0].values.length > 1 and (chart_type != 'discreteBarChart'))
|
|
if chart_type == 'stackedAreaChart'
|
|
chart = nv.models.stackedAreaChart().useInteractiveGuideline(true)
|
|
else
|
|
chart = nv.models.lineChart().useInteractiveGuideline(true)
|
|
|
|
if data.length > 0
|
|
if $scope.display.interval == 'day'
|
|
setTimeScale(chart.xAxis, chart.xScale, [d3.time.day, data[0].values.length])
|
|
else if $scope.display.interval == 'week'
|
|
setTimeScale(chart.xAxis, chart.xScale, [d3.time.week, data[0].values.length])
|
|
else if $scope.display.interval == 'month'
|
|
setTimeScale(chart.xAxis, chart.xScale, [d3.time.month, data[0].values.length])
|
|
|
|
chart.xAxis.tickFormat(xAxisTickFormatFunction)
|
|
chart.yAxis.tickFormat(d3.format('d'))
|
|
|
|
chart.xAxis.axisLabel(X_AXIS_LABEL)
|
|
chart.yAxis.axisLabel(Y_AXIS_LABEL)
|
|
|
|
# only one date, display histograms
|
|
else
|
|
chart = nv.models.discreteBarChart()
|
|
chart.tooltip.enabled(false)
|
|
chart.showValues(true)
|
|
chart.x (d) -> d.label
|
|
chart.y (d) -> d.value
|
|
data = prepareDataForBarChart(data, type)
|
|
|
|
# common for each charts
|
|
chart.margin({left: 100, right: 100})
|
|
chart.noData(_t('no_data_for_this_period'))
|
|
chart.height( CHART_HEIGHT )
|
|
|
|
# add new chart to the page
|
|
d3.select(id).datum(data).transition().duration(350).call(chart)
|
|
|
|
# resize the graph when the page is resized
|
|
nv.utils.windowResize(chart.update)
|
|
# return the chart
|
|
chart
|
|
|
|
|
|
|
|
##
|
|
# Given an NVD3 line chart axis, scale it to display ordinated dates, according to the given arguments
|
|
##
|
|
setTimeScale = (nvd3Axis, nvd3Scale, argsArray) ->
|
|
scale = d3.time.scale()
|
|
|
|
nvd3Axis.scale(scale)
|
|
nvd3Scale(scale)
|
|
|
|
if (not argsArray and not argsArray.length)
|
|
oldTicks = nvd3Axis.axis.ticks
|
|
nvd3Axis.axis.ticks = ->
|
|
oldTicks.apply(nvd3Axis.axis, argsArray)
|
|
|
|
|
|
|
|
##
|
|
# Translate line chart data in dates row to bar chart data, one bar per type.
|
|
##
|
|
prepareDataForBarChart = (data, type) ->
|
|
newData = [
|
|
key: type
|
|
values: []
|
|
]
|
|
for info in data
|
|
if info
|
|
newData[0].values.push
|
|
"label": info.key
|
|
"value": info.values[0].y
|
|
"color": info.color
|
|
|
|
newData
|
|
|
|
|
|
|
|
##
|
|
# Sort the provided array, in the specified order, on the value returned by the callback.
|
|
# This is a stable-sorting algorithm implementation, ie. two call with the same array will return the same results
|
|
# orders, especially with equal values.
|
|
# @param array {Array} the array to sort
|
|
# @param order {string} 'ASC' or 'DESC'
|
|
# @param getValue {function} the callback which will return the value on which the sort will occurs
|
|
# @returns {Array}
|
|
##
|
|
stableSort = (array, order, getValue) ->
|
|
# prepare sorting
|
|
keys_order = []
|
|
result = []
|
|
for i in [0..array.length] by 1
|
|
keys_order[array[i]] = i;
|
|
result.push(array[i]);
|
|
|
|
# callback for javascript native Array.sort()
|
|
sort_fc = (a, b) ->
|
|
val_a = getValue(a)
|
|
val_b = getValue(b)
|
|
if val_a == val_b
|
|
return keys_order[a] - keys_order[b]
|
|
if val_a < val_b
|
|
if order == 'ASC' then return -1
|
|
else return 1
|
|
else
|
|
if order == 'ASC' then return 1
|
|
else return -1
|
|
|
|
# finish the sort
|
|
result.sort(sort_fc)
|
|
return result
|
|
|
|
|
|
|
|
## !!! MUST BE CALLED AT THE END of the controller
|
|
initialize()
|
|
]
|