diff --git a/src/css/loading.less b/src/css/loading.less index 89a1028f..919bbf51 100644 --- a/src/css/loading.less +++ b/src/css/loading.less @@ -1,21 +1,21 @@ -// Copyright 2017 Telefónica Digital España S.L. -// -// This file is part of UrboCore WWW. -// -// UrboCore WWW is free software: you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// UrboCore WWW is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -// General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. -// -// For those usages not covered by this license please contact with +// Copyright 2017 Telefónica Digital España S.L. +// +// This file is part of UrboCore WWW. +// +// UrboCore WWW is free software: you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// UrboCore WWW is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +// General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. +// +// For those usages not covered by this license please contact with // iot_support at tid dot es .loading{ @@ -74,7 +74,7 @@ &.widgetL{ background-color: rgba(0, 71, 93, .8); - z-index: 999; + z-index: 3; top: 5px; left: 5px; width: ~ "calc(100% - 10px)"; diff --git a/src/css/widgets/widget.less b/src/css/widgets/widget.less index eb06a175..a51eb097 100644 --- a/src/css/widgets/widget.less +++ b/src/css/widgets/widget.less @@ -799,6 +799,7 @@ margin-bottom: 0 !important; cursor: default; padding: 20px; + padding-bottom: 40px; button { margin-top: 16px; display: block; diff --git a/src/css/widgets/widget_multiVariable_chart.less b/src/css/widgets/widget_multiVariable_chart.less index db27309a..a22af2c0 100644 --- a/src/css/widgets/widget_multiVariable_chart.less +++ b/src/css/widgets/widget_multiVariable_chart.less @@ -49,13 +49,6 @@ padding-top: 0; min-height: 280px; transform: translateY(-10px); - &.normalized{ - .nv-y{ - text{ - display: none; - } - } - } &.without-data { min-height: 306px; } diff --git a/src/js/App.js b/src/js/App.js index 0e19a3b0..79dbf985 100644 --- a/src/js/App.js +++ b/src/js/App.js @@ -302,7 +302,7 @@ App.nl2br = function nl2br(str, is_xhtml) { */ App.formatDateTime = function(date, format){ if (typeof format === 'undefined') { - format = 'DD/MM/YYYY HH:mm'; + format = 'DD/MM/YYYY - HH:mm'; } return moment.utc(date).local().format(format); } @@ -613,6 +613,13 @@ $(function() { $('body').on('click', 'a', function (e){ var attr = $(this).attr('jslink'); var href = $(this).attr('href'); + var blank = $(this).attr('target') === '_blank'; + + if (blank) { + if (!href.includes('/' + App.lang + '/')) + $(this).attr('href', '/' + App.lang + href); + return; + } //Prevent update url history when clicking a link to the current page if (href && href.slice(1) === Backbone.history.getFragment()) { diff --git a/src/js/Collection/BaseCollection.js b/src/js/Collection/BaseCollection.js index 21c7e551..c36f7d8f 100644 --- a/src/js/Collection/BaseCollection.js +++ b/src/js/Collection/BaseCollection.js @@ -32,23 +32,36 @@ App.Collection.Base = Backbone.Collection.extend({ } this.options = options; + + // To change the attribute "data" (string), + // inside the payload, to JSON object + this.on('sync', _.bind(function (response) { + if (response.options && + response.options.data && + typeof response.options.data === 'string') { + this.options.data = JSON.parse(response.options.data); + } + }, this)); } }); App.Collection.Post = App.Collection.Base.extend({ fetch: function (options) { - options = _.defaults(options, { + // We transforms the options to JSON to merge with other options + if (typeof options !== 'undefined' && typeof options.data === 'string') { + options.data = JSON.parse(options.data); + } + // Default values + options = _.defaults(options || {}, { type: 'POST', contentType: 'application/json', }); - // Add initial model options - options = _.extend(this.options || {}, options); + options = _.extend({}, this.options || {}, options); - if (options.data) { - if (typeof options.data !== 'string') { - options.data = JSON.stringify(options.data); - } + // We transform to STRING to send in the requests + if (typeof options.data !== 'string') { + options.data = JSON.stringify(options.data); } return Backbone.Collection.prototype.fetch.call(this, options); diff --git a/src/js/Collection/DeviceCollection.js b/src/js/Collection/DeviceCollection.js index c2c643e7..d83b4441 100644 --- a/src/js/Collection/DeviceCollection.js +++ b/src/js/Collection/DeviceCollection.js @@ -1,20 +1,20 @@ // Copyright 2017 Telefónica Digital España S.L. -// +// // This file is part of UrboCore WWW. -// +// // UrboCore WWW is free software: you can redistribute it and/or // modify it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. -// +// // UrboCore WWW is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero // General Public License for more details. -// +// // You should have received a copy of the GNU Affero General Public License // along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. -// +// // For those usages not covered by this license please contact with // iot_support at tid dot es @@ -159,7 +159,6 @@ App.Collection.DeviceTimeSerie = Backbone.Collection.extend({ } }); -// App.Collection.DeviceTimeSerieChart = App.Collection.DeviceTimeSerie.extend({ App.Collection.DeviceTimeSerieChart = Backbone.Collection.extend({ initialize: function (models, options) { @@ -180,9 +179,17 @@ App.Collection.DeviceTimeSerieChart = Backbone.Collection.extend({ var stepOption = options.data && options.data.time ? options.data.time.step || this.options.step : this.options.step; + // Change step aggregation + var currentAggOptions = options.data && Array.isArray(options.data.agg) + ? options.data.agg + : this.options.agg; + // Change noData option + var noData = options.data && options.data.noData + ? options.data.noData + : this.options.data.noData || false options.data = JSON.stringify({ - agg: _.map(this.options.agg, function (val) { return val }), + agg: _.map(currentAggOptions, function (val) { return val }), vars: this.options.vars, time: { start: App.ctx.getDateRange().start, @@ -194,7 +201,8 @@ App.Collection.DeviceTimeSerieChart = Backbone.Collection.extend({ condition: { id_entity__eq: this.options.id } - } + }, + noData: noData }); return Backbone.Collection.prototype.fetch.call(this, options); @@ -206,11 +214,6 @@ App.Collection.DeviceTimeSerieChart = Backbone.Collection.extend({ _.each(response, function (r) { _.each(Object.keys(r.data), function (k) { - // Prepare the date ("time" parameter) - var currentTime = r.time.split('T'); - var currentTimeDay = currentTime[0]; - var currentTimeHour = currentTime[1].replace('.000Z', ''); - if (!aux[k]) { aux[k] = []; } @@ -218,9 +221,9 @@ App.Collection.DeviceTimeSerieChart = Backbone.Collection.extend({ r.data[k] = 0; } aux[k].push({ - x: moment(currentTimeDay + ' ' + currentTimeHour).toDate(), - y: k === 'seconds' - ? r.data[k] / 60 + x: moment.utc(r.time).toDate(), + y: k === 'seconds' + ? r.data[k] / 60 : r.data[k] }); }); @@ -250,12 +253,8 @@ App.Collection.DeviceRaw = App.Collection.Post.extend({ }, fetch: function (options) { - if (typeof options === 'undefined') { - var options = {} - } - // Default options - options = _.defaults(options, { + options = _.defaults(options || {}, { data: {} }); @@ -263,11 +262,6 @@ App.Collection.DeviceRaw = App.Collection.Post.extend({ options.data = JSON.parse(options.data) } - // Options "format" - if (typeof options.data.format === 'undefined' && this.options.format) { - options.data.format = this.options.format; - } - // Options "time" if (typeof options.data.time === 'undefined') { options.data.time = App.ctx.getDateRange(); diff --git a/src/js/Collection/DevicesGroupTimeserieCollection.js b/src/js/Collection/DevicesGroupTimeserieCollection.js index 15425d7c..1a66add0 100644 --- a/src/js/Collection/DevicesGroupTimeserieCollection.js +++ b/src/js/Collection/DevicesGroupTimeserieCollection.js @@ -1,71 +1,76 @@ -// Copyright 2017 Telefónica Digital España S.L. -// -// This file is part of UrboCore WWW. -// -// UrboCore WWW is free software: you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// UrboCore WWW is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -// General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. -// -// For those usages not covered by this license please contact with +// Copyright 2017 Telefónica Digital España S.L. +// +// This file is part of UrboCore WWW. +// +// UrboCore WWW is free software: you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// UrboCore WWW is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +// General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. +// +// For those usages not covered by this license please contact with // iot_support at tid dot es 'use strict'; App.Collection.DevicesGroupTimeserie = Backbone.Collection.extend({ - initialize: function(models,options) { - this.options = options; - }, + initialize: function (models, options) { + this.options = options; + }, - url: function(){ - return App.config.api_url + '/' + this.options.scope +'/variables/' + this.options.variable + '/devices_group_timeserie' - }, + url: function () { + return App.config.api_url + '/' + this.options.scope + '/variables/' + this.options.variable + '/devices_group_timeserie' + }, - parse: function(response) { + parse: function (response) { - var aux = {}; + var aux = {}; - _.each(response, function(r) { - _.each(Object.keys(r.data), function(k) { - if(!aux[k]) - aux[k] = []; - if(r.data[k] != null) - aux[k].push({'x':new Date(r.time), 'y':k == 'seconds' ? r.data[k]/60:r.data[k]}); - }); - }); + _.each(response, function (r) { + _.each(Object.keys(r.data), function (k) { + if (!aux[k]) + aux[k] = []; + if (r.data[k] != null) + aux[k].push({ + x: moment.utc(r.time).toDate(), + y: k == 'seconds' + ? r.data[k] / 60 + : r.data[k] + }); + }); + }); - response = _.map(aux, function(values, key){ - return {'key':key, 'values':values, 'disabled':false} - }); + response = _.map(aux, function (values, key) { + return { 'key': key, 'values': values, 'disabled': false } + }); - return response; - }, + return response; + }, - fetch: function(options) { + fetch: function (options) { - options = options || {}; + options = options || {}; - var date = App.ctx.getDateRange(); - options['data'] = { - 'start': date.start, - 'finish': date.finish, - 'id_variable':this.options.variable, - 'step':this.options.step, - 'agg':this.options.agg, - 'groupagg':true - }; + var date = App.ctx.getDateRange(); + options['data'] = { + 'start': date.start, + 'finish': date.finish, + 'id_variable': this.options.variable, + 'step': this.options.step, + 'agg': this.options.agg, + 'groupagg': true + }; - if(App.ctx.get('bbox')) - options['data']['bbox'] = App.ctx.get('bbox'); + if (App.ctx.get('bbox')) + options['data']['bbox'] = App.ctx.get('bbox'); - return Backbone.Collection.prototype.fetch.call(this, options); - } + return Backbone.Collection.prototype.fetch.call(this, options); + } }); diff --git a/src/js/Collection/SearchMapCollection.js b/src/js/Collection/SearchMapCollection.js index b57e266a..eeecdcc3 100644 --- a/src/js/Collection/SearchMapCollection.js +++ b/src/js/Collection/SearchMapCollection.js @@ -1,21 +1,21 @@ -// Copyright 2017 Telefónica Digital España S.L. -// -// This file is part of UrboCore WWW. -// -// UrboCore WWW is free software: you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// UrboCore WWW is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -// General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. -// -// For those usages not covered by this license please contact with +// Copyright 2017 Telefónica Digital España S.L. +// +// This file is part of UrboCore WWW. +// +// UrboCore WWW is free software: you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// UrboCore WWW is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +// General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. +// +// For those usages not covered by this license please contact with // iot_support at tid dot es App.Collection.SearchMap = Backbone.Collection.extend({ @@ -39,4 +39,12 @@ App.Collection.SearchMap = Backbone.Collection.extend({ return Backbone.Collection.prototype.fetch.call(this, options); } -}); \ No newline at end of file +}); + +App.Collection.SearchMapExtended = App.Collection.Post.extend({ + url: function () { + return App.config.api_url + + '/' + this.options.scope + +'/entities/search/extended'; + } +}) \ No newline at end of file diff --git a/src/js/Collection/TabletoCsvCollection.js b/src/js/Collection/TabletoCsvCollection.js index 4ef96737..c1751c4a 100644 --- a/src/js/Collection/TabletoCsvCollection.js +++ b/src/js/Collection/TabletoCsvCollection.js @@ -26,7 +26,11 @@ App.Collection.TableToCsv = Backbone.Collection.extend({ initialize: function (models, options) { - this.options = options; + this.options = _.defaults(options || {}, { + data_tz: 'Europe/Madrid', //to get the correct date attribute + dataType: 'text', + format: 'csv', + }); }, /** diff --git a/src/js/Collection/Variables.js b/src/js/Collection/Variables.js index ea139b33..7012a41a 100644 --- a/src/js/Collection/Variables.js +++ b/src/js/Collection/Variables.js @@ -37,15 +37,16 @@ App.Collection.Variables.Timeserie = App.Collection.Post.extend({ this.options.data = { agg: this.options.agg, - vars: this.options.vars, csv: this.options.csv || false, + filters: this.options.filters || {}, findTimes: this.options.findTimes || false, + noData: this.options.noData || false, time: { start: date.start, finish: date.finish, step: this.options.step }, - filters: this.options.filters || {} + vars: this.options.vars, }; } @@ -124,7 +125,7 @@ App.Collection.Variables.TimeserieGrouped = App.Collection.Post.extend({ aux[d.agg] = []; } aux[d.agg].push({ - x: new Date(r.time), + x: moment.utc(r.time).toDate(), y: d.value }); }); diff --git a/src/js/Model/Context.js b/src/js/Model/Context.js index 8f82b971..d8d1fd38 100644 --- a/src/js/Model/Context.js +++ b/src/js/Model/Context.js @@ -91,8 +91,9 @@ App.Model.Context = Backbone.Model.extend({ getDateRange: function () { try { return { - start: moment(this.get('start').utc().toDate()).format('YYYY-MM-DD HH:mm:ss'), - finish: moment(this.get('finish').utc().toDate()).format('YYYY-MM-DD') + ' 23:59:59' + start: moment(this.get('start')).toISOString(), + finish: moment(moment.utc(this.get('finish')).toDate()) + .endOf('day').toISOString() } } catch (err) { return false; diff --git a/src/js/Utils.js b/src/js/Utils.js index 83aeaaac..37eb511c 100644 --- a/src/js/Utils.js +++ b/src/js/Utils.js @@ -379,6 +379,24 @@ App.Utils.toCamelCase = function(string) { }); } + /** + * Get the multiples number to a number + * @param {Number} number + * @return {Array} multiples numbers + */ + App.Utils.getMultipleNumbers = function (number) { + if (typeof number === 'undefined' || Number.isNaN(number)) return []; + + var multiples = []; + for (var i = 1; i <= number; i++) { + if (number % i === 0) { + multiples.push(i); + } + } + + return multiples; + }, + /** * Generate the "script" tag from different files to load them dynamically * @@ -428,7 +446,7 @@ App.Utils.loadBlockedScripts = function(type, cb) { /** * Set an array to a string to use * in a contidion 'IN' to SQL query - * + * * @param {Array} data - string array * @return {String | null} - string to use in query SQL */ diff --git a/src/js/View/DateView.js b/src/js/View/DateView.js index 8cacb043..97ecd6de 100644 --- a/src/js/View/DateView.js +++ b/src/js/View/DateView.js @@ -219,8 +219,6 @@ App.View.Date = Backbone.View.extend({ ? lastDateMinusRange : moment(this.options.minDate); - // var lastDate = moment(this.$('.date.finish').datepicker('getDate')).startOf('day'); - // var firstDate = lastDate.clone().subtract(this.options.maxRange); inst.settings.minDate = firstDate.toDate(); inst.settings.maxDate = lastDate.toDate(); } else if (input.classList.contains('finish')) { @@ -234,8 +232,6 @@ App.View.Date = Backbone.View.extend({ ? firstDateAddRange : moment(this.options.maxDate); - // var firstDate = moment(this.$('.date.start').datepicker('getDate')).startOf('day'); - // var lastDate = firstDate.clone().add(this.options.maxRange); inst.settings.minDate = firstDate.toDate(); inst.settings.maxDate = lastDate.toDate(); } else { diff --git a/src/js/View/Map/Layer/MapboxGLLayer.js b/src/js/View/Map/Layer/MapboxGLLayer.js index bd5a663c..317345cd 100644 --- a/src/js/View/Map/Layer/MapboxGLLayer.js +++ b/src/js/View/Map/Layer/MapboxGLLayer.js @@ -29,9 +29,9 @@ App.View.Map.Layer.MapboxGLLayer = Backbone.View.extend({ dataSource: null, layers: [], popupTemplate: new App.View.Map.MapboxGLPopup('#map-mapbox_base_popup_template'), - - initialize: function(model, body, legend, map) { + + initialize: function (model, body, legend, map) { this._map = map; this._model = model; this.legendConfig = legend; @@ -50,58 +50,58 @@ App.View.Map.Layer.MapboxGLLayer = Backbone.View.extend({ this.addToLegend(); }, - addToLegend: function() { + addToLegend: function () { if (this.legendConfig) { this._map.addToLegend(this.legendConfig); } }, - updateData: function(body) { + updateData: function (body) { this._model.clear(); - this._model.fetch({data: body}); + this._model.fetch({ data: body }); }, - on: function(event, ids, callback) { - if(this._mapEvents[event] === undefined) { + on: function (event, ids, callback) { + if (this._mapEvents[event] === undefined) { this._mapEvents[event] = {}; } - if(ids.constructor === Array) { + if (ids.constructor === Array) { ids.forEach(id => { - this._map._map.on(event,id,callback); + this._map._map.on(event, id, callback); this._mapEvents[event][id] = callback; }); - }else { - this._map._map.on(event,ids,callback); - this._mapEvents[event][ids] = callback; + } else { + this._map._map.on(event, ids, callback); + this._mapEvents[event][ids] = callback; } return this; }, - offAll: function() { - _.each(this._mapEvents, function(childs,event) { - _.each(childs, function(callback, name) { - this._map._map.off(event,name,callback); + offAll: function () { + _.each(this._mapEvents, function (childs, event) { + _.each(childs, function (callback, name) { + this._map._map.off(event, name, callback); }.bind(this)) }.bind(this)) }, - onClose: function() { + onClose: function () { this.offAll(); }, /** * @deprecated */ - setInteractivity: function(label, properties = [], deviceViewLink = false) { + setInteractivity: function (label, properties = [], deviceViewLink = false) { console.warn('setInteractivity is DEPRECATED. Please use setPopup instead.') - this.on('click',this.layers.map(l => l.id), function(e) { + this.on('click', this.layers.map(l => l.id), function (e) { let mpopup = new mapboxgl.Popup() - .setLngLat(e.lngLat); - if(deviceViewLink) { - deviceViewLink = deviceViewLink.replace('{{device}}',e.features[0].properties.id_entity); + .setLngLat(e.lngLat); + if (deviceViewLink) { + deviceViewLink = deviceViewLink.replace('{{device}}', e.features[0].properties.id_entity); } mpopup.setHTML(this.popupTemplate - .drawTemplate(label,properties, e, mpopup, deviceViewLink)).addTo(this._map._map); + .drawTemplate(label, properties, e, mpopup, deviceViewLink)).addTo(this._map._map); }.bind(this)); return this; }, @@ -111,7 +111,7 @@ App.View.Map.Layer.MapboxGLLayer = Backbone.View.extend({ * - output: HTML Output * - classes: String of classes for parent. */ - setPopup: function(classes, label, templates = []) { + setPopup: function (classes, label, templates = []) { // If it exists the attribute "hasPopup", we only apply // the "Popup" a these layers otherwise all layers // will have "Popup" @@ -119,44 +119,61 @@ App.View.Map.Layer.MapboxGLLayer = Backbone.View.extend({ ? this.layers.filter(l => l.hasPopup) : this.layers; - this.on('click', layersWithPopups.map(l => l.id), function(e) { + this.on('click', layersWithPopups.map(l => l.id), function (e) { let mpopup = new mapboxgl.Popup() - .setLngLat(e.lngLat); + .setLngLat(e.lngLat); var fullyProcessedTemplate = this.popupTemplate - .drawTemplatesRow(classes,label,templates, e, mpopup) + .drawTemplatesRow(classes, label, templates, e, mpopup) mpopup.setHTML(fullyProcessedTemplate).addTo(this._map._map); - + }.bind(this)); return this; }, - setHoverable: function(isHoverable) { + setHoverable: function (isHoverable) { if (isHoverable) { - this.on('mouseenter',_.map(this.layers,function(l) {return l.id}), function() { + this.on('mouseenter', _.map(this.layers, function (l) { return l.id }), function () { this._map._map.getCanvas().style.cursor = 'pointer'; }.bind(this)); - this.on('mouseleave',_.map(this.layers,function(l) {return l.id}), function() { + this.on('mouseleave', _.map(this.layers, function (l) { return l.id }), function () { this._map._map.getCanvas().style.cursor = ''; }.bind(this)); } return this; }, - _success: function(change) { - this.dataSource = (change.changed.type)? change.changed : {type: "FeatureCollection", features: []}, - this._map.getSource(this._idSource).setData(this.dataSource); - this._map._sources.find(function(src) { - return src.id === this._idSource; - }.bind(this)).data = {'type': 'geojson', 'data': this.dataSource}; + _success: function (change) { + this.dataSource = (change.changed.type) + ? change.changed + : { + type: 'FeatureCollection', + features: change.changed.features || [] + }, + + // Change the data in layer + this._map.getSource(this._idSource) + .setData(this.dataSource); + this._map._sources + .find(function (src) { + return src.id === this._idSource; + }.bind(this)) + .data = { + type: 'geojson', + data: this.dataSource + }; + + // The event is launched + this.trigger('update', { id: this._idSource } ); + return change; }, - _error: function() { + _error: function () { console.error("Error"); } - + }); diff --git a/src/js/View/Map/Layer/MapboxGLVectorLayer.js b/src/js/View/Map/Layer/MapboxGLVectorLayer.js index 5b00a772..a8eaeb2e 100644 --- a/src/js/View/Map/Layer/MapboxGLVectorLayer.js +++ b/src/js/View/Map/Layer/MapboxGLVectorLayer.js @@ -29,7 +29,7 @@ App.View.Map.Layer.MapboxGLVectorLayer = App.View.Map.Layer.MapboxGLLayer.extend dataSource: null, layers: [], - initialize: function(model, body, legend, map) { + initialize: function (model, body, legend, map) { this._map = map; this._model = model; this.legendConfig = legend; @@ -46,52 +46,67 @@ App.View.Map.Layer.MapboxGLVectorLayer = App.View.Map.Layer.MapboxGLLayer.extend this.addToLegend(); }, - addToLegend: function() { + addToLegend: function () { if (this.legendConfig) { this._map.addToLegend(this.legendConfig); } }, - on: function(event, ids, callback) { - if(this._mapEvents[event] === undefined) { + on: function (event, ids, callback) { + if (this._mapEvents[event] === undefined) { this._mapEvents[event] = {}; } - if(ids.constructor === Array) { + if (ids.constructor === Array) { ids.forEach(id => { - this._map._map.on(event,id,callback); + this._map._map.on(event, id, callback); this._mapEvents[event][id] = callback; }); - }else { - this._map._map.on(event,ids,callback); - this._mapEvents[event][ids] = callback; + } else { + this._map._map.on(event, ids, callback); + this._mapEvents[event][ids] = callback; } return this; }, - offAll: function() { - _.each(this._mapEvents, function(childs,event) { - _.each(childs, function(callback, name) { - this._map._map.off(event,name,callback); + offAll: function () { + _.each(this._mapEvents, function (childs, event) { + _.each(childs, function (callback, name) { + this._map._map.off(event, name, callback); }.bind(this)) }.bind(this)) }, - onClose: function() { + onClose: function () { this.offAll(); }, - _success: function(change) { - this.dataSource = (change.changed.type)? change.changed : {type: "FeatureCollection", features: []}, - this._map.getSource(this._idSource).setData(this.dataSource); - this._map._sources.find(function(src) { - return src.id === this._idSource; - }.bind(this)).data = {'type': 'geojson', 'data': this.dataSource}; + _success: function (change) { + this.dataSource = (change.changed.type) + ? change.changed + : { + type: 'FeatureCollection', + features: [] + }, + + // Change the data in layer + this._map.getSource(this._idSource) + .setData(this.dataSource); + this._map._sources + .find(function (src) { + return src.id === this._idSource; + }.bind(this)) + .data = { + type: 'geojson', + data: this.dataSource + }; + + // The event is launched + this.trigger('update', { id: this._idSource } ); + return change; }, - _error: function() { + _error: function () { console.error("Error"); } - - }); diff --git a/src/js/View/Map/Layer/MapboxSQLLayer.js b/src/js/View/Map/Layer/MapboxSQLLayer.js index f92fa21f..e5528fa9 100644 --- a/src/js/View/Map/Layer/MapboxSQLLayer.js +++ b/src/js/View/Map/Layer/MapboxSQLLayer.js @@ -23,7 +23,7 @@ App.View.Map.Layer.MapboxSQLLayer = App.View.Map.Layer.MapboxGLVectorLayer.extend({ - initialize: function(config) { + initialize: function (config) { this.legendConfig = config.legend; this.layers = config.layers; this._ignoreOnLegend = config.ignoreOnLegend; @@ -32,39 +32,45 @@ App.View.Map.Layer.MapboxSQLLayer = App.View.Map.Layer.MapboxGLVectorLayer.exten App.View.Map.Layer.MapboxGLVectorLayer.prototype .initialize.call(this, config.source.model, - config.source.payload,config.legend, config.map); + config.source.payload, config.legend, config.map); }, - _layersConfig: function() { + _layersConfig: function () { return this.layers; }, - _success: function(model) { - var response = (model.changed)? model.changed.response : undefined; + _success: function (model) { + var response = (model.changed) + ? model.changed.response + : undefined; + if (response) { var cartoLayer = response; var nStyle = this._map._map.getStyle(); + // Change the data in layer if (nStyle && nStyle.sources && nStyle.sources[this._idSource]) { nStyle.sources[this._idSource].tiles = cartoLayer.metadata.tilejson.vector.tiles; - this._map._map.setStyle(nStyle); + this._map._map.setStyle(nStyle); + + // The event is launched + this.trigger('update', { id: this._idSource } ); } } }, - _updateSQLData: function(sql, sourceLayer) { + _updateSQLData: function (sql, sourceLayer) { sourceLayer = sourceLayer || 'cartoLayer' this._model.clear(); this._model.params = { - layers:[ { + layers: [{ id: sourceLayer, - options:{ + options: { sql: sql } }] }; - - this._model.fetch({ - }); + + this._model.fetch({}); }, }); diff --git a/src/js/View/Map/LayerTreeView.js b/src/js/View/Map/LayerTreeView.js index 3825a20f..06e7b76d 100644 --- a/src/js/View/Map/LayerTreeView.js +++ b/src/js/View/Map/LayerTreeView.js @@ -1,21 +1,21 @@ -// Copyright 2017 Telefónica Digital España S.L. -// -// This file is part of UrboCore WWW. -// -// UrboCore WWW is free software: you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// UrboCore WWW is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -// General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. -// -// For those usages not covered by this license please contact with +// Copyright 2017 Telefónica Digital España S.L. +// +// This file is part of UrboCore WWW. +// +// UrboCore WWW is free software: you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// UrboCore WWW is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +// General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. +// +// For those usages not covered by this license please contact with // iot_support at tid dot es App.View.Map.LayerTree.View = Backbone.View.extend({ @@ -25,7 +25,7 @@ App.View.Map.LayerTree.View = Backbone.View.extend({ initialize: function(options) { this.options= _.defaults(options,{ - title: __('Filtros'), + title: __('Ajustes'), compact: false }); if(this.options.collection.first().get('legendData')){ diff --git a/src/js/View/Map/MapBoxSearchView.js b/src/js/View/Map/MapBoxSearchView.js index d55defac..80cf2c01 100644 --- a/src/js/View/Map/MapBoxSearchView.js +++ b/src/js/View/Map/MapBoxSearchView.js @@ -43,14 +43,21 @@ App.View.MapBoxSearch = App.View.MapSearch.extend({ this.listenTo(this._collection, "reset", this._collectionReset); + this.listenTo(this.options.filterModel, 'change:currentStatus', function(){ + this._marker.remove() + if (this.$('input').val().length > 0){ + this._updateTerm() + } + }) + }, _selectTerm: function (e) { this.$('#search_map').addClass('searching'); - this.$('input[type=text]').val($(e.currentTarget).text()); + this.$('input[type=text]').val(''); this.$('ul').removeClass('active'); - var elem = this._collection.findWhere({ 'element_id': $(e.currentTarget).attr('element_id') }); - var bbox = elem.get('bbox'); + var elem = this._collection.findWhere({ 'id_entity': $(e.currentTarget).attr('element_id') }); + var bbox = elem.get('geometry'); var maxZoom = 18; if (elem.get('type') == 'device') { maxZoom = 19; @@ -59,7 +66,30 @@ App.View.MapBoxSearch = App.View.MapSearch.extend({ this._marker.setLngLat([bbox[0], bbox[1]]) .addTo(this._map); - this._map.fitBounds([[bbox[0], bbox[1]], [bbox[2], bbox[3]]], { maxZoom: maxZoom }); + this._map.fitBounds([[bbox[0], bbox[1]], [bbox[0], bbox[1]]], { maxZoom: maxZoom }); + }, + + _updateTerm: _.debounce(function (e) { + var term = e + ? $(e.currentTarget).val() + : this.$('input').val(); + this._collection.options.term = term + this._collection.fetch({ + 'reset': true, + data: this.getCurrentFilterParams(term) || {}}); + if (this._collection.options.term.length > 0) { + this.$('#search_map').addClass('searching'); + this.$('.loading').remove(); + this.$('#search_map').append(App.circleLoading()); + } else { + this.$('img').trigger('click'); + } + }, 500), + + // This function is meant to be overrided on each vertical since + // filtering requires special data treatment + getCurrentFilterParams(){ + return null }, _clearSearch: function () { @@ -69,6 +99,6 @@ App.View.MapBoxSearch = App.View.MapSearch.extend({ this.$('#search_map').removeClass('searching'); this._marker.remove() - } + }, }); diff --git a/src/js/View/Map/MapboxGLMapView.js b/src/js/View/Map/MapboxGLMapView.js index 3f05442e..16ba1af8 100644 --- a/src/js/View/Map/MapboxGLMapView.js +++ b/src/js/View/Map/MapboxGLMapView.js @@ -72,7 +72,7 @@ App.View.Map.MapboxView = Backbone.View.extend({ this.$el.append(this.button3d); this.$el.append(this.zoomControl); this.filterModel = options.filterModel; - + this.listenTo(App.ctx,'change:bbox_status',this._changeBBOXStatus); this.listenTo(App.ctx, 'change:start change:finish', function() { if (options.filterModel) { @@ -133,6 +133,16 @@ App.View.Map.MapboxView = Backbone.View.extend({ // Override for bbox changes actions. }, + /** + * Reset BBox + */ + _resetBBox: function () { + // Reset BBOX + App.ctx.set('bbox', null); + App.ctx.set('bbox_info', false); + App.ctx.set('bbox_status', false); + }, + _onMapLoaded: function() { // This event is called after map loaded. // Place your layers here. @@ -146,6 +156,9 @@ App.View.Map.MapboxView = Backbone.View.extend({ this.stopListening(); this.basemapSelector.close(), this.legend.close(); + + // Reset BBOX + this._resetBBox(); }, addSource: function(idSource, dataSource) { diff --git a/src/js/View/widgets/Charts/BarsLineD3Chart.js b/src/js/View/widgets/Charts/BarsLineD3Chart.js index 46398538..2ad1324a 100644 --- a/src/js/View/widgets/Charts/BarsLineD3Chart.js +++ b/src/js/View/widgets/Charts/BarsLineD3Chart.js @@ -29,223 +29,167 @@ * - showLineDots (Boolean, default: false). Show dots on lines without hover */ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ - initialize: function(options){ + initialize: function (options) { - if(!options.opts.has('keysConfig')) throw new Error('keysConfig parameter is mandatory'); - if(!options.opts.has('showLineDots')) options.opts.set({showLineDots: false}); - if(!options.opts.has('interpolate')) options.opts.set({interpolate: 'monotone'}); + if (!options.opts.has('keysConfig')) throw new Error('keysConfig parameter is mandatory'); + if (!options.opts.has('showLineDots')) options.opts.set({ showLineDots: false }); + if (!options.opts.has('interpolate')) options.opts.set({ interpolate: 'monotone' }); - App.View.Widgets.Charts.Base.prototype.initialize.call(this,options); + App.View.Widgets.Charts.Base.prototype.initialize.call(this, options); _.bindAll(this, '_getColor'); }, - _drawChart:function(){ + _drawChart: function () { // Process data this._processData(); // Create chart this._initChartModel(); - // // Order data keys for legend - // if(this.options.get('legendOrderFunc')) - // this._orderLegendKeys(); - // - // // Append data to chart - // d3.select(this.$('.chart')[0]) - // .datum(this.data) - // .call(this._chart); - // // Create legend this._initLegend(); // Create tooltips this._initTooltips(); - // // Adjustments - // if(this.options.get('centerLegend')) - // this._centerLegend(); - // - // if(this.options.get('xAxisLabel')) - // this._chart.xAxis.axisLabel(this.options.get('xAxisLabel')); - // - // if(this.options.get('yAxisLabel')) - // if(this._chart.yAxis){ - // this._chart.yAxis.axisLabel(this.options.get('yAxisLabel')); - // }else if(this.options.get('yAxisLabel').length >= 2){ - // this._chart.yAxis1.axisLabel(this.options.get('yAxisLabel')[0]); - // this._chart.yAxis2.axisLabel(this.options.get('yAxisLabel')[1]); - // } - // - // if(this.xAxisFunction) - // this._formatXAxis(); - // - // if(this.options.get('yAxisFunction')) - // this._formatYAxis(); - // - // if(this.options.has('xAxisDomain')) - // this._forceXAxisDomain(); - // - // if(this.options.has('yAxisDomain')) - // this._forceYAxisDomain(); - // - // if(this.options.get('yAxisAdjust')) - // this._adjustYAxis(); - // - // // Force apply adjustments (TODO: fix this hack) - // var _this = this; - // setTimeout(function(){ - // _this._chart.update(); - // },100); - // - // nv.utils.windowResize(this._chart.update); - // Remove loading animation this.$('.loading.widgetL').addClass('hiden'); }, - _processData: function(){ - // Extract colors - this._colors = this.options.get('colors'); + _processData: function () { + var min = []; + var max = []; - // Format data - var tempData = this.collection.toJSON(); + this._colors = this.options.get('colors');// Extract colors this.data = []; - var min = [], - max = []; - var _this = this; - _.each(tempData, function(elem, dataIdx){ - // Check aggregations - if(elem.values && elem.values.length && elem.values[0].y && elem.values[0].y.constructor === Array){ - _.each(elem.values[0].y, function(subelem, subElemIdx){ + + this.data = _.reduce(this.collection.toJSON(), function (sumElements, elem, dataIdx) { + // value 'Y' is an Array + if (elem.values && + elem.values.length && + elem.values[0].y && + elem.values[0].y.constructor === Array) { + _.each(elem.values[0].y, function (subelem, subElemIdx) { var key = elem.key + '_' + subelem.agg.toLowerCase(); var procSubelem = { - key: (_this.options.get('legendNameFunc') && _this.options.get('legendNameFunc')(elem.key)) ? _this.options.get('legendNameFunc')(elem.key) + ' (' + subelem.agg + ')' : key, - agg: subelem.agg, - realKey: key, - type: _this.options.get('keysConfig')[key].type, - yAxis: _this.options.get('keysConfig')[key].axis, - values: [] + key: (this.options.get('legendNameFunc') && this.options.get('legendNameFunc')(elem.key)) + ? this.options.get('legendNameFunc')(elem.key) + ' (' + subelem.agg + ')' + : key, + agg: subelem.agg, + realKey: key, + type: this.options.get('keysConfig')[key].type, + yAxis: this.options.get('keysConfig')[key].axis, + values: [] }; - if(elem.values && elem.values.length && elem.values[0].x){ + if (elem.values && elem.values.length && elem.values[0].x) { var axis = procSubelem.yAxis - 1; + min[axis] = min[axis] || []; max[axis] = max[axis] || []; - var i = 0; - if(elem.values[0].x.constructor == Date){ - var timeFormatter = d3.time.format.iso; - _.each(elem.values, function(value, idx){ - procSubelem.values[idx] = { - x: timeFormatter.parse(value.x), - y: value.y[subElemIdx].value - }; - if(_this.options.get('stacked')){ - max[axis][i] = max[axis][i] ? max[axis][i] + procSubelem.values[idx].y : procSubelem.values[idx].y; - }else{ - max[axis].push(procSubelem.values[idx].y) - } - min[axis].push(procSubelem.values[idx].y); - i += 1; - }); - }else{ - _.each(elem.values, function(value, idx){ - procSubelem.values[idx] = { - x: value.x, - y: value.y[subElemIdx].value - }; - if(_this.options.get('stacked')){ - max[axis][i] = max[axis][i] ? max[axis][i] + procSubelem.values[idx].y : procSubelem.values[idx].y; - }else{ - max[axis].push(procSubelem.values[idx].y) - } - min[axis].push(procSubelem.values[idx].y); - i += 1; - }); - } + + _.each(elem.values, function (value, idx) { + procSubelem.values[idx] = { + // x: value.x === Date + // ? d3.time.format.iso.parse(value.x) + // : value.x, + y: value.y[subElemIdx].value + }; + + if (this.options.get('stacked')) { + max[axis][idx] = max[axis][idx] + ? max[axis][idx] + procSubelem.values[idx].y + : procSubelem.values[idx].y; + } else { + max[axis].push(procSubelem.values[idx].y) + } + + min[axis].push(procSubelem.values[idx].y); + }.bind(this)); + + // Add element to array + sumElements.push(procSubelem); } + }.bind(this)); + } else { // value 'Y' is NOT an Array - _this.data.push(procSubelem); - }); - }else{ + // save the original value elem.realKey = elem.key; - if(_this.options.get('legendNameFunc') && _this.options.get('legendNameFunc')(elem.key, elem)) - elem.key = _this.options.get('legendNameFunc')(elem.key, elem); - if (_this.options.get('keysConfig')[elem.realKey]) { - elem.type = _this.options.get('keysConfig')[elem.realKey].type; - elem.yAxis = _this.options.get('keysConfig')[elem.realKey].axis; + if (this.options.get('legendNameFunc') && this.options.get('legendNameFunc')(elem.key, elem)) { + elem.key = this.options.get('legendNameFunc')(elem.key, elem); + } + + if (this.options.get('keysConfig')[elem.realKey]) { + elem.type = this.options.get('keysConfig')[elem.realKey].type; + elem.yAxis = this.options.get('keysConfig')[elem.realKey].axis; } else { - elem.type = _this.options.get('keysConfig')['*'].type; - elem.yAxis = _this.options.get('keysConfig')['*'].axis + elem.type = this.options.get('keysConfig')['*'].type; + elem.yAxis = this.options.get('keysConfig')['*'].axis } - if(elem.values && elem.values.length && elem.values[0].x && elem.values[0].x.constructor == Date){ - var timeFormatter = d3.time.format.iso; - var axis = elem.yAxis - 1; - min[axis] = min[axis] || []; - max[axis] = max[axis] || []; - var i = 0; - _.each(elem.values, function(value){ - value.x = timeFormatter.parse(value.x); - if(_this.options.get('stacked')){ - max[axis][i] = max[axis][i] ? max[axis][i] + value.y : value.y; - }else{ - max[axis].push(value.y); - } - min[axis].push(value.y); - i += 1; - }); - } else if(elem.values && elem.values.length && elem.values[0].x && elem.values[0].x.constructor != Date) { + if (elem.values && elem.values.length && elem.values[0].x) { var axis = elem.yAxis - 1; + min[axis] = min[axis] || []; max[axis] = max[axis] || []; - var i = 0; - _.each(elem.values, function(value){ - if(_this.options.get('stacked')){ - max[axis][i] = max[axis][i] ? max[axis][i] + value.y : value.y; - }else{ + + _.each(elem.values, function (value, index) { + // // value 'X' axis is a Date + // value.x = value.x.constructor === Date + // ? d3.time.format.iso.parse(value.x) + // : value.x; + + if (this.options.get('stacked')) { + max[axis][index] = max[axis][index] + ? max[axis][index] + value.y + : value.y; + } else { max[axis].push(value.y); } min[axis].push(value.y); - i += 1; - }); + }.bind(this)); + + // Add element to array + sumElements.push(elem); } - _this.data.push(elem); } - }); + + return sumElements; + }.bind(this), []); // Sort data to bring lines to the end - this.data.sort(function(a, b){ + this.data.sort(function (a, b) { return a.type > b.type; }); // Get max value for each axis and adjust domain - var domains = [[0,1]]; - if(this.options.get('yAxisDomain')) { + var domains = [[0, 1]]; + if (this.options.get('yAxisDomain')) { domains = JSON.parse(JSON.stringify(this.options.get('yAxisDomain'))) - } else if(this.data.length > 1){ - domains.push([0,1]); + } else if (this.data.length > 1) { + domains.push([0, 1]); } // is possible to force the domains if (this.options.get('yAxisDomainForce') !== true) { var minAxis1 = Math.min.apply(null, min[0]), - minAxis2 = Math.min.apply(null, min[1]); + minAxis2 = Math.min.apply(null, min[1]); var maxAxis1 = Math.max.apply(null, max[0]), - maxAxis2 = Math.max.apply(null, max[1]); + maxAxis2 = Math.max.apply(null, max[1]); - if(domains[0][0] > minAxis1) domains[0][0] = Math.floor(minAxis1); - if(domains[0][1] < maxAxis1) domains[0][1] = Math.ceil(maxAxis1); - if(domains[1]) { - if(domains[1][0] > minAxis2) domains[1][0] = Math.floor(minAxis2); - if(domains[1][1] < maxAxis2) domains[1][1] = Math.ceil(maxAxis2); + if (domains[0][0] > minAxis1) domains[0][0] = Math.floor(minAxis1); + if (domains[0][1] < maxAxis1) domains[0][1] = Math.ceil(maxAxis1); + if (domains[1]) { + if (domains[1][0] > minAxis2) domains[1][0] = Math.floor(minAxis2); + if (domains[1][1] < maxAxis2) domains[1][1] = Math.ceil(maxAxis2); } } this.yAxisDomain = domains; }, - _initChartModel: function(){ + _initChartModel: function () { // FIX BUG at fire_detection vertical // When the graph redraws its height it's not // property setted if the legend bar is already present @@ -253,14 +197,14 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ // is setted bigger than usual. Prefarably, the graph should update once on each request. // Actually does several times at least for this vertical. - if(this.$('.var_list .tags').length > 0){ + if (this.$('.var_list .tags').length > 0) { this.$('.var_list .tags').remove() } // Clean all this.$('.chart').empty(); this._chart = {}; - this._chart.margin = this.options.get('margin') || {top: 40, right: 80, bottom: 90, left: 80}; + this._chart.margin = this.options.get('margin') || { top: 40, right: 50, bottom: 90, left: 60 }; if (this.options.get('hideYAxis2')) { this._chart.margin.right = 40; } @@ -278,21 +222,21 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ .attr('height', this._chart.h + (this._chart.margin.top + this._chart.margin.bottom)) .attr('class', 'chart d3') .append('g') - .attr('transform', 'translate(' + this._chart.margin.left + ',' + this._chart.margin.top + ')') + .attr('transform', 'translate(' + this._chart.margin.left + ',' + this._chart.margin.top + ')') - if(this.options.get('stacked')){ + if (this.options.get('stacked')) { this.stackedRawData = {}; - _.each(this.data, function(el){ - if(el.type === 'bar' && ! _this._internalData.disabledList[el.realKey]){ + _.each(this.data, function (el) { + if (el.type === 'bar' && !_this._internalData.disabledList[el.realKey]) { _this.stackedRawData[el.realKey] = el; // el.values } }); - this.stackedData = _.map(this.stackedRawData, function(val, key){ + this.stackedData = _.map(this.stackedRawData, function (val, key) { return val; }); } - if(this.data[1]) { + if (this.data[1]) { this.xScaleBars = d3.scale.ordinal() .domain(d3.range(this.data[1].values.length)) .rangeRoundBands([0, this._chart.w], this.options.get('groupSpacing')); @@ -302,7 +246,7 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ .rangeRoundBands([0, this._chart.w], this.options.get('groupSpacing')); } - this.xScaleLine = function(d) { + this.xScaleLine = function (d) { var offset = _this.xScaleBars.rangeBand() / 2; return _this.xScaleBars(d) + offset; }; @@ -315,7 +259,7 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ .domain(this.yAxisDomain[0]) .range([this._chart.h, 0]) ] - if(this.yAxisDomain[1]) { + if (this.yAxisDomain[1]) { this.yScales.push( d3.scale[scale2Fn]() .domain(this.yAxisDomain[1]) @@ -329,14 +273,14 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ // Line generator this.lineGen = d3.svg.line() - .x(function(d, idx) { + .x(function (d, idx) { return _this.xScaleLine(idx); }) - .y(function(d, idx) { + .y(function (d, idx) { return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y); }) .interpolate(this.options.get('interpolate')) - ; + ; // Draw if (this._chart.xAxis) { @@ -355,12 +299,12 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ .remove(); xAxis - .append("image") - .attr("xlink:href", function(d, i) { + .append('image') + .attr('xlink:href', function (d, i) { return _this.options.get('yAxisTickFormat')(_this.data[0].values[d].x); }) - .attr("width", 16) - .attr("height", 16) + .attr('width', 16) + .attr('height', 16) .attr('x', -8) .attr('y', 8); } @@ -368,51 +312,68 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ var yAxis1 = this._chart.svg.append('g') .attr('class', 'axis y-axis y-axis-1') .call(this._chart.yAxis1); - if(this.options.get('yAxisLabel')){ + var currentyAxisLabel1 = Array.isArray(this.options.get('yAxisLabel')) + ? this.options.get('yAxisLabel')[0] + : ''; + + // If only it exists a line (variable) to draw in the chart + // we put in the yAxis1 (left) the information + // about of this variable + if(this.options.get('showMetaVariableYAxis') && + Array.isArray(this.data) && + this.data.length -1 === this._internalData.elementsDisabled) { + var variable = _.find(this.data, function (currentVariable) { + return !this._internalData.disabledList[currentVariable.realKey]; + }.bind(this)); + var variableMetadata = App.mv().getVariable(variable.realKey).toJSON(); + + currentyAxisLabel1 = variableMetadata.name + ' (' + variableMetadata.units + ')'; + } + + if (this.options.get('yAxisLabel') || this.options.get('showMetaVariableYAxis')) { yAxis1.append('text') .attr('class', 'axis-label') .attr('x', -1 * this._chart.h / 2) - .attr('transform', 'rotate(270) translate(0,'+ (12 - _this._chart.margin.left) +')') + .attr('transform', 'rotate(270) translate(0,' + (12 - _this._chart.margin.left) + ')') .style('text-anchor', 'middle') - .text(_this.options.get('yAxisLabel')[0]) - ; + .text(currentyAxisLabel1); } - if(this.yAxisDomain[1] && !this.options.get('hideYAxis2')){ + if (this.yAxisDomain[1] && !this.options.get('hideYAxis2')) { var yAxis2 = this._chart.svg.append('g') .attr('class', 'axis y-axis y-axis-2') .attr('transform', 'translate(' + this._chart.w + ',0)') .call(this._chart.yAxis2); - if(this.options.get('yAxisLabel')){ + if (this.options.get('yAxisLabel')) { yAxis2.append('text') .attr('class', 'axis-label') .attr('x', this._chart.h / 2) - .attr('transform', 'rotate(90) translate(0,-68)') + .attr('transform', 'rotate(90) translate(0, -40)') .style('text-anchor', 'middle') .text(this.options.get('yAxisLabel')[1]) - ; + ; } } - if(this.options.has('yAxisThresholds')){ + if (this.options.has('yAxisThresholds')) { // Format thresholds var _this = this; var ticks = d3.selectAll(this.$('g.axis.y-axis-1 g.tick line')); ticks - .attr('style', function(d,i){ + .attr('style', function (d, i) { var style = ''; var thresholdCfg = _this.options.get('yAxisThresholds')[i]; - if(thresholdCfg){ + if (thresholdCfg) { style += 'stroke-dasharray: 4; stroke: ' + thresholdCfg.color; - }else if(_this.options.get('yAxisThresholds').length){ + } else if (_this.options.get('yAxisThresholds').length) { style += 'stroke-dasharray: 4; stroke: ' + _this.options.get('yAxisThresholds')[_this.options.get('yAxisThresholds').length - 1].color; } return style; }); - for(var i = 0; i < ticks[0].length -1;i++){ - var y = ticks[0][i+1].getCTM().f - this._chart.margin.top; + for (var i = 0; i < ticks[0].length - 1; i++) { + var y = ticks[0][i + 1].getCTM().f - this._chart.margin.top; var width = ticks[0][i].getBoundingClientRect().width; - var height = ticks[0][i].getCTM().f - ticks[0][i+1].getCTM().f; + var height = ticks[0][i].getCTM().f - ticks[0][i + 1].getCTM().f; var g = this._chart.svg.append('g'); g.append('rect') .attr('x', 0) @@ -421,7 +382,7 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ .attr('height', height) .attr('fill', this.options.get('yAxisThresholds')[i].color) .attr('style', 'opacity: .1') - ; + ; g.append('text') .text(__(this.options.get('yAxisThresholds')[i].realName)) .attr('class', 'axis-label') @@ -431,13 +392,13 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ .attr('width', width) .attr('height', height / 2) .attr('class', 'thresholdLabel') - ; + ; } } this._drawElements(); } else { - // Remove every "g" element + // Remove every 'g' element d3.select(this.$('.chart')[0]).selectAll('g').remove(); // Without data (CSS) d3.select(this.$('.chart')[0]) @@ -446,30 +407,34 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ .attr('x', '50%') .attr('y', '50%') .style('text-anchor', 'middle') - .text(__('No hay datos disponibles')); + .text( + this.options.has('customNoDataMessage') + ? this.options.get('customNoDataMessage') + : __('No hay datos disponibles') + ); } }, - _drawElements: function() { + _drawElements: function () { var _this = this; - this.data.forEach(function(data){ + this.data.forEach(function (data) { switch (data.type) { case 'bar': - if(!_this._internalData.disabledList[data.realKey]){ - if(_this.options.get('stacked')){ + if (!_this._internalData.disabledList[data.realKey]) { + if (_this.options.get('stacked')) { _this._drawStackedBar(_this.stackedData); - }else{ + } else { _this._drawSimpleBar(data); } } break; case 'line': case 'line-dash': - if(!_this._internalData.disabledList[data.realKey]){ + if (!_this._internalData.disabledList[data.realKey]) { _this._drawLine(data, { lineDash: data.type === 'line-dash' }); } case 'point': - if(!_this._internalData.disabledList[data.realKey]){ + if (!_this._internalData.disabledList[data.realKey]) { _this._drawPoint(data); } } @@ -482,7 +447,7 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ * @param {Array} data - data to draw the line * @param {Object} options - options to draw the line */ - _drawLine: function(data, options){ + _drawLine: function (data, options) { this._chart.line = this._chart.line || []; options = _.defaults(options, {}); @@ -490,23 +455,23 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ var line = this._chart.svg.append('g').selectAll('.lineGroup') .data([data]).enter() .append('g') - .attr('class', 'lineGroup') - .attr('key', function(d){ return d.realKey; }) - .style('fill', function(d, idx) { return _this._getColor(this.__data__, idx); }) - .style('stroke', function(d, idx) { return _this._getColor(this.__data__, idx); }); + .attr('class', 'lineGroup') + .attr('key', function (d) { return d.realKey; }) + .style('fill', function (d, idx) { return _this._getColor(this.__data__, idx); }) + .style('stroke', function (d, idx) { return _this._getColor(this.__data__, idx); }); // Draw the line (path) line.append('path') - .datum(data.values) - .attr('class', function(d, idx){ - var extraClass = _this._getClasses(this.parentElement.__data__, idx); - return 'line ' + extraClass; - }) - .attr('style', 'fill: none') - .style('stroke', function(d, idx) { - return _this._getColor(this.parentElement.__data__, idx); - }) - .attr('d', this.lineGen); + .datum(data.values) + .attr('class', function (d, idx) { + var extraClass = _this._getClasses(this.parentElement.__data__, idx); + return 'line ' + extraClass; + }) + .attr('style', 'fill: none') + .style('stroke', function (d, idx) { + return _this._getColor(this.parentElement.__data__, idx); + }) + .attr('d', this.lineGen); // line-dash if (options.lineDash) { @@ -517,16 +482,16 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ line.selectAll('.point') .data(data.values).enter() .append('circle') - .attr('class', 'point') - .attr('cx', function(d, idx) { return _this.xScaleLine(idx); }) - .attr('cy', function(d, idx) { return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y); }) - .attr('r', 3) - .attr('data-y', function(d, idx) { return d.y }); + .attr('class', 'point') + .attr('cx', function (d, idx) { return _this.xScaleLine(idx); }) + .attr('cy', function (d, idx) { return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y); }) + .attr('r', 3) + .attr('data-y', function (d, idx) { return d.y }); this._chart.line.push(line); }, - _drawSimpleBar: function(data){ + _drawSimpleBar: function (data) { var _this = this; this._chart.bars = this._chart.bars || []; // this._chart.bars = this._chart.svg.selectAll('.bar') @@ -534,26 +499,26 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ bar.selectAll('.bar') .data(data.values).enter() .append('rect') - .attr('class', 'bar') - .attr('x', function(d, idx) {return _this.xScaleBars(idx);}) - .attr('y', function(d, idx) { - return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y); - }) - .attr('width', function(d) {return _this.xScaleBars.rangeBand();}) - .attr('height', function(d) {return _this._chart.h - _this.yScales[this.parentElement.__data__.yAxis - 1](d.y);}) - .style('fill', function(d,idx){ - return _this._getColor(this.parentElement.__data__, idx); - }) - .attr('data-idx', function(d, idx) {return idx; }); + .attr('class', 'bar') + .attr('x', function (d, idx) { return _this.xScaleBars(idx); }) + .attr('y', function (d, idx) { + return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y); + }) + .attr('width', function (d) { return _this.xScaleBars.rangeBand(); }) + .attr('height', function (d) { return _this._chart.h - _this.yScales[this.parentElement.__data__.yAxis - 1](d.y); }) + .style('fill', function (d, idx) { + return _this._getColor(this.parentElement.__data__, idx); + }) + .attr('data-idx', function (d, idx) { return idx; }); this._chart.bars.push(bar); }, - _drawStackedBar: function(data){ + _drawStackedBar: function (data) { var _this = this; this._chart.bars = this._chart.bars || []; var layers = d3.layout.stack() - .values(function(d){ return d.values }) + .values(function (d) { return d.values }) (data); var bar = this._chart.svg.append('g').data([data]); @@ -561,22 +526,23 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ bar = this._chart.svg.selectAll('.layer') .data(layers) .enter().append('g') - .attr('class', 'layer') - .attr('key', function(d){ return d.realKey; }) - .style('fill', function(d,idx){ - return _this._getColor(d, idx); - }); + .attr('class', 'layer') + .attr('key', function (d) { return d.realKey; }) + .style('fill', function (d, idx) { + return _this._getColor(d, idx); + }); bar.selectAll('rect') - .data(function(d) { - return d.values; }) + .data(function (d) { + return d.values; + }) .enter().append('rect') - .attr('x', function(d, idx) { return _this.xScaleBars(idx); }) - .attr('y', function(d, idx) { return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y + d.y0); }) - .attr('height', function(d) { - return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y0) - _this.yScales[this.parentElement.__data__.yAxis - 1](d.y + d.y0); - }) - .attr('width', _this.xScaleBars.rangeBand() - 1) - .attr('data-idx', function(d, idx) {return idx; }) + .attr('x', function (d, idx) { return _this.xScaleBars(idx); }) + .attr('y', function (d, idx) { return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y + d.y0); }) + .attr('height', function (d) { + return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y0) - _this.yScales[this.parentElement.__data__.yAxis - 1](d.y + d.y0); + }) + .attr('width', _this.xScaleBars.rangeBand() - 1) + .attr('data-idx', function (d, idx) { return idx; }) ; this._chart.bars.push(bar); }, @@ -586,41 +552,48 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ * * @param {Array} data - data to show */ - _drawPoint: function(data){ + _drawPoint: function (data) { var _this = this; this._chart.line = this._chart.line || []; var line = this._chart.svg.append('g').selectAll('.lineGroup') .data([data]).enter() .append('g') - .attr('class', 'lineGroup') - .attr('key', function(d){ return d.realKey; }) - .style('fill', function(d, idx) { return _this._getColor(this.__data__, idx); }) - .style('stroke', function(d, idx) { return _this._getColor(this.__data__, idx); }); + .attr('class', 'lineGroup') + .attr('key', function (d) { return d.realKey; }) + .style('fill', function (d, idx) { return _this._getColor(this.__data__, idx); }) + .style('stroke', function (d, idx) { return _this._getColor(this.__data__, idx); }); line.selectAll('.point') .data(data.values).enter() .append('circle') - .attr('class', 'point') - .attr('cx', function(d, idx) { return _this.xScaleLine(idx); }) - .attr('cy', function(d, idx) { return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y); }) - // Radius circle - .attr('r', function(d, idx) { - var radiusFunction = _this.options.get('radiusFunction'); - - if (typeof radiusFunction === 'function') { - return radiusFunction(d, idx); - } + .attr('class', 'point') + .attr('cx', function (d, idx) { return _this.xScaleLine(idx); }) + .attr('cy', function (d, idx) { return _this.yScales[this.parentElement.__data__.yAxis - 1](d.y); }) + // Radius circle + .attr('r', function (d, idx) { + var radiusFunction = _this.options.get('radiusFunction'); + + if (typeof radiusFunction === 'function') { + return radiusFunction(d, idx); + } - return 3; // Default value - }); + return 3; // Default value + }); this._chart.line.push(line); }, - _formatXAxis: function(){ - if(this.data.length && this.data[0].values && this.data[0].values.length && this.data[0].values[0].x && this.data[0].values[0].x.constructor == Date){ + _formatXAxis: function () { + if (this.data.length && + this.data[0].values && + this.data[0].values.length && + this.data[0].values[0].x && + this.data[0].values[0].x.constructor === Date) { + var _this = this; var start = moment(this.data[0].values[0].x).startOf('hour'); - var finish = moment(this.data[0].values[this.data[0].values.length - 1].x).endOf('hour').add(1, 'millisecond'); + var finish = moment(this.data[0].values[this.data[0].values.length - 1].x) + .endOf('hour') + .add(1, 'millisecond'); var diff = parseInt(finish.diff(start, 'hours') / 6); // Diff / Default number of ticks // Fix the changes in models and collections (BaseModel & BaseCollections) @@ -630,15 +603,15 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ // Get step hours var stepDiff = -1; - if(this.collection.options.data.time && this.collection.options.data.time.step){ + if (this.collection.options.data.time && this.collection.options.data.time.step) { stepDiff = App.Utils.getStepHours(this.collection.options.data.time.step); - if(diff !== -1){ - diff = Math.ceil(diff/stepDiff) * stepDiff; + if (diff !== -1) { + diff = Math.ceil(diff / stepDiff) * stepDiff; } } // Adjustments if (diff === 0) diff = 1; // If diff is 0 => Infinite loop in lines 538-541 (do-while) - if(diff > 6 && diff < 12) diff = 12; + if (diff > 6 && diff < 12) diff = 12; // Manually create dates range var datesInterval = []; @@ -646,37 +619,49 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ do { datesInterval.push(nextDate); nextDate = d3.time.hour.offset(nextDate, diff); - }while(finish.isAfter(nextDate) && diff > 0); + } while (finish.isAfter(nextDate) && diff > 0); + - var _this = this; this._chart.xAxis = d3.svg.axis() .scale(this.xScaleBars) .orient('bottom') - .tickFormat(function(d) { + .tickFormat(function (d) { var absStepDiff = Math.abs(stepDiff); - - if(_this.data[0].values.length > datesInterval.length + 1){ - if((d*absStepDiff) % diff === 0 && (d*absStepDiff/diff) < datesInterval.length) - return _this.xAxisFunction(datesInterval[d*absStepDiff/diff]); - else + var formatDate = _this._getStepRange() === 'd' + ? App.formatDate + : App.formatDateTime; + + if (_this.data[0].values.length > datesInterval.length + 1) { + if ((d * absStepDiff) % diff === 0 && (d * absStepDiff / diff) < datesInterval.length) { + return typeof _this.xAxisFunction === 'function' + ? _this.xAxisFunction(datesInterval[d * absStepDiff / diff]) + : datesInterval[d * absStepDiff / diff] instanceof Date + ? formatDate(datesInterval[d * absStepDiff / diff]) + : datesInterval[d * absStepDiff / diff]; + } else { return ''; - }else if(d < datesInterval.length){ - return _this.xAxisFunction(datesInterval[d]); - }else{ + } + } else if (d < datesInterval.length) { + return typeof _this.xAxisFunction === 'function' + ? _this.xAxisFunction(datesInterval[d]) + : datesInterval[d] instanceof Date + ? formatDate(datesInterval[d]) + : datesInterval[d]; + } else { return ''; } }) // .tickValues(datesInterval) .tickSize([]) .tickPadding(10) - ; + ; - } else if(this.data.length && this.data[0].values && this.data[0].values.length && this.data[0].values[0].x && this.data[0].values[0].x.constructor != Date){ + } else if (this.data.length && this.data[0].values && this.data[0].values.length && this.data[0].values[0].x && this.data[0].values[0].x.constructor != Date) { var _this = this; this._chart.xAxis = d3.svg.axis() .scale(this.xScaleBars) .orient('bottom') - .tickFormat(function(d) { + .tickFormat(function (d) { return _this.options.get('yAxisTickFormat')(_this.data[0].values[d].x); }) .tickSize([]) @@ -684,7 +669,7 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ } }, - _formatYAxis: function() { + _formatYAxis: function () { // Force domains and different between lines (range) // Added a method to alter diff value from verticals // if you need fixed incremental steps @@ -693,14 +678,15 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ ? this.options.get('yAxisDomainDiff') || 1 : (this.yAxisDomain[0][1] - this.yAxisDomain[0][0]) / 4 var range; - if(!this.options.has('yAxisThresholds')){ + if (!this.options.has('yAxisThresholds')) { range = d3.range( this.yAxisDomain[0][0], this.yAxisDomain[0][1], this.options.get('yAxisStep') || diff ); - }else { - range = _.pluck(this.options.get('yAxisThresholds'),'startValue').sort(function(a,b){ return a - b; }); + } else { + range = _.pluck(this.options.get('yAxisThresholds'), 'startValue') + .sort(function (a, b) { return a - b; }); } range.push(this.yAxisDomain[0][1]); @@ -708,15 +694,15 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ .scale(this.yScales[0]) .orient('left') .tickValues(range) - .tickSize(-1 * this._chart.w ,0) + .tickSize(-1 * this._chart.w, 0) .tickPadding(10); - if(!this.options.get('hideYAxis1')) { + if (!this.options.get('hideYAxis1')) { this._chart.yAxis1.tickFormat(this.options.get('yAxisFunction')[0]); } else { this._chart.yAxis1.tickFormat(''); } - if(this.yAxisDomain[1]){ + if (this.yAxisDomain[1]) { diff = (this.yAxisDomain[1][1] - this.yAxisDomain[1][0]) / 4; range = d3.range( this.yAxisDomain[1][0], @@ -734,27 +720,27 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ } }, - _getColor: function(d,idx){ - if(typeof this.options.get('colors') === 'function'){ + _getColor: function (d, idx) { + if (typeof this.options.get('colors') === 'function') { return this.options.get('colors')(d, idx); - }else if(this.options.get('colors') && this.options.get('colors').length > 0){ + } else if (this.options.get('colors') && this.options.get('colors').length > 0) { return this.options.get('colors')[idx]; - }else{ + } else { return '#333'; } }, - _getClasses: function(d,idx){ - if(typeof this.options.get('classes') === 'function'){ + _getClasses: function (d, idx) { + if (typeof this.options.get('classes') === 'function') { return this.options.get('classes')(d, idx); - }else if(this.options.get('classes') && this.options.get('classes').length > 0){ + } else if (this.options.get('classes') && this.options.get('classes').length > 0) { return this.options.get('classes')[idx]; - }else{ + } else { return ''; } }, - _initLegend: function() { + _initLegend: function () { if (!this.options.get('hideLegend')) { this.$('.var_list').html(this._list_variable_template({ colors: this.options.get('colors'), @@ -766,70 +752,76 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ } }, - _clickLegend: function(element) { - var tags = $(".btnLegend").size(); - var realKey = $(element.target).closest("div").attr("id"); - + _clickLegend: function (element) { + var tags = $('.btnLegend').size(); + var realKey = $(element.target).closest('div').attr('id'); var disabledList = this._internalData.disabledList; - if(((disabledList[realKey] == undefined || disabledList[realKey] === false) && this._internalData.elementsDisabled != tags - 1) || disabledList[realKey] === true) { - // var active = $(element.target).parent().hasClass('inactive') ? false : true, - // newOpacity = active ? 0 : 1; - // - // d3.select(this.$('g[key=' + realKey + ']')[0]).style('opacity', newOpacity); - // $(element.target).parent().toggleClass('inactive'); + if (((disabledList[realKey] === undefined || disabledList[realKey] === false) && + this._internalData.elementsDisabled != tags - 1) || disabledList[realKey] === true) { disabledList[realKey] = !disabledList[realKey]; - this._internalData.elementsDisabled = disabledList[realKey] ? this._internalData.elementsDisabled + 1 : this._internalData.elementsDisabled - 1; - + this._internalData.elementsDisabled = disabledList[realKey] + ? this._internalData.elementsDisabled + 1 + : this._internalData.elementsDisabled - 1; // Force redraw this._drawChart(); } }, - _initTooltips: function(){ + _initTooltips: function () { var _this = this; - if(this._chart.bars){ - this._chart.bars.forEach(function(barchart){ + if (this._chart.bars) { + this._chart.bars.forEach(function (barchart) { _this._setTooltipEvents(barchart.selectAll('rect'), _this); }); } - if(this._chart.line) { - this._chart.line.forEach(function(lineGroup){ + if (this._chart.line) { + this._chart.line.forEach(function (lineGroup) { _this._setTooltipEvents(lineGroup.selectAll('.point'), _this); }); } }, - _setTooltipEvents: function(elem, _this){ + _setTooltipEvents: function (elem, _this) { elem - .on('mouseover', function(d, serie, index){ - _this._drawTooltip(d, serie, index, this); - }) - .on('mousemove', function(d, serie, index){ - _this._drawTooltip(d, serie, index, this); + .on('mouseover', function (d, serie, index) { + _this._drawTooltip(d, serie, index, this, _this); }) - .on('mouseout', function(d, serie, index){ - _this._hideTooltip(d, serie, index, this); + .on('mousemove', function (d, serie, index) { + _this._drawTooltip(d, serie, index, this, _this); }) - ; + .on('mouseout', function (d, serie, index) { + _this._hideTooltip(d, serie, index, this, _this); + }) + ; }, - _drawTooltip: function(d, serie, index, _this){ - var $tooltip = this.$('#chart_tooltip'); - if(!$tooltip.length){ - $tooltip = $('
'); - this.$el.append($tooltip); - } + _drawTooltip: function (d, serie, index, _this, view) { + // Set default data var data = { value: d.x, series: [] }; + // The value in "dta.value" depends from "step" + if (typeof view.xAxisFunction !== 'function') { + data.value = view._getStepRange() === 'd' + ? App.formatDate(d.x) + : App.formatDateTime(d.x); + } + + var $tooltip = this.$('#chart_tooltip'); + + if (!$tooltip.length) { + $tooltip = $(''); + this.$el.append($tooltip); + } + var that = this; var keysConfig = that.options.get('keysConfig'); - this.data.forEach( function(el) { - if(!that._internalData.disabledList[el.realKey]){ + this.data.forEach(function (el) { + if (!that._internalData.disabledList[el.realKey]) { data.series.push({ value: typeof that.options.get('toolTipValueFunction') === 'function' ? that.options.get('toolTipValueFunction')(el.realKey, el.values[serie].y) @@ -837,8 +829,10 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ key: el.key, realKey: el.realKey, color: that._getColor(el, serie), - cssClass: that.options.has('classes') ? that.options.get('classes')(el): '', - yAxisFunction: that.options.get('yAxisFunction')[el.yAxis - 1], + cssClass: that.options.has('classes') ? that.options.get('classes')(el) : '', + yAxisFunction: that.options.has('yAxisFunctionTooltipValue') + ? that.options.get('yAxisFunctionTooltipValue') + : that.options.get('yAxisFunction')[el.yAxis - 1], type: keysConfig[el.realKey] && keysConfig[el.realKey].type ? keysConfig[el.realKey].type : 'line' @@ -853,25 +847,53 @@ App.View.Widgets.Charts.D3.BarsLine = App.View.Widgets.Charts.Base.extend({ } })); var cursorPos = d3.mouse(_this); - $tooltip.css({position: 'absolute'}); + $tooltip.css({ position: 'absolute' }); if (cursorPos[0] + $tooltip.width() > this.$el.width() - $tooltip.width()) { $tooltip.css({ top: cursorPos[1], zIndex: 2, left: cursorPos[0] - 2 * $tooltip.width() + 75 }); - }else { + } else { $tooltip.css({ top: cursorPos[1], zIndex: 2, - left: cursorPos[0] - $tooltip.width()/2 + 20 + left: cursorPos[0] - $tooltip.width() / 2 + 20 }); } $tooltip.removeClass('hidden'); }, - _hideTooltip: function() { + /** + * Get the step range choosen by the user + * + * @param {String} - One option "d" (day), "h" (hour), "m" (minute) + */ + _getStepRange: function () { + // Request data (default) + var requestData = { + time: { + step: '1d' + } + }; + + if (this.collection && this.collection.options && this.collection.options.data) { + requestData = typeof this.collection.options.data === 'string' + ? JSON.parse(this.collection.options.data) + : this.collection.options.data; + } + + // current Step + var currentStep = requestData.time && requestData.time.step + ? requestData.time.step + : '1d'; + var matchStep = /(\d+)(\w+)/g.exec(currentStep); + + return matchStep[2] || 'd'; + }, + + _hideTooltip: function () { this.$('#chart_tooltip').addClass('hidden'); }, }); diff --git a/src/js/View/widgets/Charts/BaseChart.js b/src/js/View/widgets/Charts/BaseChart.js index 5325eb92..3c54f9f2 100644 --- a/src/js/View/widgets/Charts/BaseChart.js +++ b/src/js/View/widgets/Charts/BaseChart.js @@ -108,6 +108,11 @@ App.View.Widgets.Charts.Base = Backbone.View.extend({ }, render: function () { + // Force the currentStep indicated in the begin + var forceInitialStep = this.options.has('forceInitialStep') + ? this.options.get('forceInitialStep') + : false; + if (this.options.has('currentStep')) { var dates = null; @@ -117,7 +122,8 @@ App.View.Widgets.Charts.Base = Backbone.View.extend({ this.options.set({ stepsAvailable: App.Utils.getStepsAvailable(dates) }); - if (!_.contains(this.options.get('stepsAvailable'), this.options.get('currentStep'))) { + if (!_.contains(this.options.get('stepsAvailable'), this.options.get('currentStep')) + && !forceInitialStep) { this.options.set({ currentStep: this.options.get('stepsAvailable')[this.options.get('stepsAvailable').length - 1] }); } } @@ -340,21 +346,23 @@ App.View.Widgets.Charts.Base = Backbone.View.extend({ _clickLegend: function (element) { var tags = $(".btnLegend").size(); - var realKey = $(element.target).closest("div").attr("id"); - var varMetadata = App.mv().getVariable(realKey); var orderKey = $(element.target).closest("div").attr("tag"); // Prevent dups - realKey = realKey + '_' + orderKey; - + var realKey = $(element.target).closest("div").attr("id") + '_' + orderKey; var disabledList = this._internalData.disabledList; - - var disabled = ((disabledList[realKey] === undefined || disabledList[realKey] === false) && this._internalData.elementsDisabled != tags - 1); + var disabled = ((disabledList[realKey] === undefined || disabledList[realKey] === false) + && this._internalData.elementsDisabled != tags - 1); var enabled = (disabledList[realKey] === true); if (disabled || enabled) { + ($(this.$(".chart .nv-series") + .get($(element.target) + .parent() + .attr("tag"))) + ).d3Click(); - - ($(this.$(".chart .nv-series").get($(element.target).parent().attr("tag")))).d3Click(); - $(element.target).parent().toggleClass("inactive"); + $(element.target) + .parent() + .toggleClass("inactive"); if (this.options.get('showAggSelector')) { var ch = $(this.$('.agg')[orderKey]); @@ -362,7 +370,9 @@ App.View.Widgets.Charts.Base = Backbone.View.extend({ } disabledList[realKey] = !disabledList[realKey]; - this._internalData.elementsDisabled = disabledList[realKey] ? this._internalData.elementsDisabled + 1 : this._internalData.elementsDisabled - 1; + this._internalData.elementsDisabled = disabledList[realKey] + ? this._internalData.elementsDisabled + 1 + : this._internalData.elementsDisabled - 1; } }, diff --git a/src/js/View/widgets/Charts/ComparisonChart.js b/src/js/View/widgets/Charts/ComparisonChart.js index a2f3ef0a..a00b8c30 100644 --- a/src/js/View/widgets/Charts/ComparisonChart.js +++ b/src/js/View/widgets/Charts/ComparisonChart.js @@ -163,6 +163,11 @@ App.View.Widgets.Charts.Comparison = App.View.Widgets.Charts.Base.extend({ if(_this.options.get('legendNameFunc') && _this.options.get('legendNameFunc')(elem.key)) elem.key = __(_this.options.get('legendNameFunc')(elem.key)); + // Fix the changes in models and collections (BaseModel & BaseCollections) + if (collection && collection.options && typeof collection.options.data === 'string') { + collection.options.data = JSON.parse(collection.options.data); + } + // Add date to key elem.key += ' ' + App.formatDate(collection.options.data.time.start) + ' - ' + App.formatDate(collection.options.data.time.finish); diff --git a/src/js/View/widgets/CustomDeviceRawTable.js b/src/js/View/widgets/CustomDeviceRawTable.js index 1ee200e5..c8f40c62 100644 --- a/src/js/View/widgets/CustomDeviceRawTable.js +++ b/src/js/View/widgets/CustomDeviceRawTable.js @@ -1,20 +1,20 @@ // Copyright 2017 Telefónica Digital España S.L. -// +// // This file is part of UrboCore WWW. -// +// // UrboCore WWW is free software: you can redistribute it and/or // modify it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. -// +// // UrboCore WWW is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero // General Public License for more details. -// +// // You should have received a copy of the GNU Affero General Public License // along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. -// +// // For those usages not covered by this license please contact with // iot_support at tid dot es @@ -86,8 +86,8 @@ App.View.Widgets.CustomDeviceRawTable = App.View.Widgets.Base.extend({ _.map(this.options.variables, function (variable) { //Set Title - if(typeof variable.title === 'undefined'){ - var targetVariable = _.find( variablesWithMetadata, function( varWithMeta ){ + if (typeof variable.title === 'undefined') { + var targetVariable = _.find(variablesWithMetadata, function (varWithMeta) { return varWithMeta.id === variable.id }); variable['title'] = targetVariable @@ -161,12 +161,7 @@ App.View.Widgets.CustomDeviceRawTable = App.View.Widgets.Base.extend({ }, dateFn: function (value) { - var dateTime = value.split('T') - var date = dateTime[0].split('-') - - return date.reverse().join('-') - + ' ' - + dateTime[1].slice(0, 5); + return App.formatDateTime(value); }, getVariableUnits: function (id) { diff --git a/src/js/View/widgets/WidgetAlertsVariable.js b/src/js/View/widgets/WidgetAlertsVariable.js index a2d14944..2b80bcdf 100644 --- a/src/js/View/widgets/WidgetAlertsVariable.js +++ b/src/js/View/widgets/WidgetAlertsVariable.js @@ -42,7 +42,7 @@ App.View.Widgets.AlertsVariable = Backbone.View.extend({ buttonclick: function(e) { if(this.options.buttonlink) { - window.location.href = this.options.buttonlink; + window.open(this.options.buttonlink, '_blank'); } }, diff --git a/src/js/View/widgets/WidgetBase.js b/src/js/View/widgets/WidgetBase.js index 5d1eaf6c..2a051209 100644 --- a/src/js/View/widgets/WidgetBase.js +++ b/src/js/View/widgets/WidgetBase.js @@ -23,6 +23,8 @@ App.View.Widgets.Base = Backbone.View.extend({ _template: _.template($('#widgets-widget_base_template').html()), + _template_timemode_historic: _.template($('#widgets-widget_date_template').html()), + _template_timemode_now: _.template($('#widgets-widget_time_template').html()), initialize: function (options) { this.options = options; @@ -60,7 +62,7 @@ App.View.Widgets.Base = Backbone.View.extend({ if (!this.model.get('embed')) { if (this.model.get('timeMode') == 'historic') { - this.listenTo(this.ctx, 'change:start change:finish', this.refresh); + this.listenTo(this.ctx, 'change:start change:finish', _.debounce(this.refresh, 600)); } this.listenTo(this.ctx, 'change:bbox', this.refresh); } @@ -294,6 +296,9 @@ App.View.Widgets.Base = Backbone.View.extend({ render: function () { this.$el.html(this._template(this.model.toJSON())); + // Put the time icon into the widget + this.drawTimeIcon(); + this.updateFilters(); for (var i in this.subviews) { @@ -310,6 +315,25 @@ App.View.Widgets.Base = Backbone.View.extend({ return this; }, + /** + * Draw the time icon in the widget + */ + drawTimeIcon: function () { + var wrapper = this.$el.find('.botons'); + var timeMode = this.model.get('timeMode'); + + if (wrapper.length && (timeMode === 'historic' || timeMode === 'now')) { + var templateTimeIcon = timeMode === 'historic' + ? this._template_timemode_historic + : this._template_timemode_now; + + // Remove element DOM + this.$el.find('#timeIcon').remove(); + // Add template time Icon + $(wrapper[0]).prepend(templateTimeIcon); + } + }, + onClose: function () { for (var i in this.subviews) { this.subviews[i].close(); diff --git a/src/js/View/widgets/WidgetGaugeView.js b/src/js/View/widgets/WidgetGaugeView.js index aef42ec5..bfb3a2de 100644 --- a/src/js/View/widgets/WidgetGaugeView.js +++ b/src/js/View/widgets/WidgetGaugeView.js @@ -1,283 +1,289 @@ -// Copyright 2017 Telefónica Digital España S.L. -// -// This file is part of UrboCore WWW. -// -// UrboCore WWW is free software: you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// UrboCore WWW is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero -// General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. -// -// For those usages not covered by this license please contact with +// Copyright 2017 Telefónica Digital España S.L. +// +// This file is part of UrboCore WWW. +// +// UrboCore WWW is free software: you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// UrboCore WWW is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +// General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with UrboCore WWW. If not, see http://www.gnu.org/licenses/. +// +// For those usages not covered by this license please contact with // iot_support at tid dot es 'use strict'; App.View.Widgets.Gauge = Backbone.View.extend({ - _template: _.template( $('#widgets-widget_gauge_template').html() ), - - initialize: function(options) { - - this.options = _.defaults(options,{ - fetchModel:false, - global:false - }); - - this.model = options.model; - this.className = this.model.get('className'); - }, - - - onClose: function(){ - this.stopListening(); - }, - - render: function(){ - var _this = this; - var metaData = App.Utils.toDeepJSON(App.mv().getVariable(this.model.get('var_id'))); - - this.$el.html(this._template({m: this.model.toJSON(), 'metaData':metaData, 'fetchModel':this.options.fetchModel})); - - this.$el.addClass(this.model.get('className')); - - if(!this.options.fetchModel){ - this._draw(metaData); - }else{ - this.model.fetch({ - data:this.model.get('data'), - success:function(data){ - _this.model.set('var_value',data.get('value')); - _this._draw(metaData); - _this.$('.co_value .value').html(App.nbf(data.get('value')) + '' + metaData.units + '') - } - }); - } - - - return this; - }, - - _draw:function(metaData){ - var _this = this; - var chart = this.$('.chart'); - - chart.html(''); - - var varRange = { - 'min':parseFloat(this.options.global ? metaData.config.global_threshold[0] : metaData.var_thresholds[0]), - 'warning':parseFloat(this.options.global ? metaData.config.global_threshold[1] : metaData.var_thresholds[1]), - 'error':parseFloat(this.options.global ? metaData.config.global_threshold[2] : metaData.var_thresholds[2]), - 'max':parseFloat(this.options.global ? metaData.config.global_threshold[3] : metaData.var_thresholds[3]) - }; - - this.model.set('var_value',this.model.get('var_value') == 'null' ? varRange.min : parseFloat(this.model.get('var_value'))); - - if(varRange['max'] < this.model.get('var_value')) - varRange['max'] = Math.ceil(this.model.get('var_value')); - - - if(varRange){ - if(metaData.reverse){ - if(this.model.get('var_value') <= varRange.error){ - this.$('.co_value .value').addClass('error'); - }else if(this.model.get('var_value') <= varRange.warning){ - this.$('.co_value .value').addClass('warning'); - } - - }else{ - if(this.model.get('var_value') >= varRange.error){ - this.$('.co_value .value').addClass('error'); - }else if(this.model.get('var_value') >= varRange.warning){ - this.$('.co_value .value').addClass('warning'); - } - } - } - - var width = 210; - var height = 160; - var r = width/2; - var ringWidth = 20; - var ringInset = 0; - var minAngle = -120; - var maxAngle = 120; - var minValue = varRange.min; - var maxValue = varRange.max; - var pointerWidth = 4; - var pointerTailLength = 0; - var pointerHeadLength = 81; - var labelFormat = d3.format(',g'); - var range = maxAngle - minAngle; - var scale = d3.scale.linear().range([0,1]).domain([minValue, maxValue]); - var ticks = [varRange.min, varRange.warning, varRange.error, varRange.max]; - - var svg = d3.select(chart[0]) - .append('svg:svg') - .attr('class', 'gauge') - .attr('width', width) - .attr('height', height); - - var centerTx = 'translate('+r +','+ r +')'; - - var arcs = svg.append('g') - .attr('class', 'arc') - .attr('transform', centerTx); - - var tickData = []; - tickData[0] = ((varRange.warning - varRange.min)/(varRange.max-varRange.min)) * range; - tickData[1] = tickData[0] + ((varRange.error - varRange.warning)/(varRange.max-varRange.min)) * range; - tickData[2] = tickData[1] + ((varRange.max - varRange.error)/(varRange.max-varRange.min)) * range; - - var arc = d3.svg.arc() - .innerRadius(r - ringWidth - ringInset) - .outerRadius(r - ringInset) - .startAngle(function(d, i) { - return _this._deg2rad(minAngle + (i == 0 ? 0: tickData[i-1])); - }) - .endAngle(function(d, i) { - return _this._deg2rad(minAngle + tickData[i]); - }) - ; - - - //TODO cambiar esto - if(metaData.reverse){ - arcs.selectAll('path') - .data(tickData) - .enter().append('path') - .attr('fill', function(d, i) { - if(i == 0){ - return App.Utils.rangeColor(App.Utils.RANGES.ERROR); - }else if(i==1){ - return App.Utils.rangeColor(App.Utils.RANGES.WARNING); - }else if(i==2){ - return App.Utils.rangeColor(App.Utils.RANGES.OK); - } - - }) - .attr('d', arc); - }else{ - arcs.selectAll('path') - .data(tickData) - .enter().append('path') - .attr('fill', function(d, i) { - if(i == 0){ - return App.Utils.rangeColor(App.Utils.RANGES.OK); - }else if(i==1){ - return App.Utils.rangeColor(App.Utils.RANGES.WARNING); - }else if(i==2){ - return App.Utils.rangeColor(App.Utils.RANGES.ERROR); - } - - }) - .attr('d', arc); - } - - - ///////////////////////////////////////////////////////////////////////////////////////// - - var lg = svg.append('g') - .attr('class', 'label') - .attr('transform', centerTx); - - lg.selectAll('text') - .data(ticks) - .enter().append('text') - .attr('transform', function(d) { - var angle = (minAngle + (scale(d) * range)) - minAngle + (minAngle + 90) - var x = Math.cos(_this._deg2rad(angle)) * (-r) * 0.64; - var y = (Math.sin(_this._deg2rad(angle)) * (-r) * 0.64) + 5; - return 'translate(' + x +',' + y +')'; - }) - .attr("text-anchor", function(d){ - var angle = (minAngle + (scale(d) * range)) - minAngle + (minAngle + 90) - var anchor = ''; - angle > 90 ? anchor='end': (angle==90 ? anchor='middle':anchor='start'); - return anchor; - }) - .text(labelFormat) - ; - - var lines = svg.append('g').attr('transform', centerTx); - for (var i = varRange.min; i <= varRange.max; i=i+((varRange.max-varRange.min)/30)) { - - var angle = (minAngle + (scale(i) * range)) - minAngle + (minAngle + 90) - var x = Math.cos(_this._deg2rad(angle)) * (-r) * 0.77; - var y = Math.sin(_this._deg2rad(angle)) * (-r) * 0.77; - - lines.append('line') - .attr("x1", x) - .attr("y1", y) - .attr("x2", x*0.9) - .attr("y2", y*0.9) - .style("stroke-width", "1px"); - }; - - - var lineData = [ [pointerWidth / 2, 0], - [0, -pointerHeadLength], - [-(pointerWidth / 2), 0], - [0, pointerTailLength], - [pointerWidth / 2, 0] ]; - - var pointerLine = d3.svg.line().interpolate('monotone'); - - var pg = svg.append('g').data([lineData]) - .attr('class', 'pointer') - .attr('transform', centerTx); - - // var pointer = pg.append('path') - // .attr('d', pointerLine) - // .attr('transform', 'rotate(' + minAngle +')'); - - svg.append("circle") - .attr("cx", r) - .attr("cy", r) - .attr("r", 5) - .style("fill", "#00475d"); - - var arc = d3.svg.arc() - .innerRadius(5) - .outerRadius(3) - .startAngle(0) - .endAngle(360); - - svg.append("path") - .attr("d", arc) - .attr('transform', 'translate(' + r + ',' + r + ')') - .attr('fill','#fff'); - - if(this.model.get('var_value') != null && this.model.get('var_value') != undefined){ - - var pointer = pg.append('path') - .attr('d', pointerLine) - .attr('transform', 'rotate(' + minAngle +')'); - - var newValue = this.model.get('var_value'); - var ratio = scale(newValue); - if (ratio < 0) { - ratio = -0.01; - } else if (ratio > 1) { - ratio = 1.01; - } - var newAngle = minAngle + (ratio * range); - pointer.attr('transform', 'rotate(-110)') - .transition() - .duration(2000) - .ease('elastic') - .attr('transform', 'rotate(' +newAngle +')'); - } - }, - - _deg2rad:function(deg){ - return deg * Math.PI / 180; - } + _template: _.template($('#widgets-widget_gauge_template').html()), + + initialize: function (options) { + + this.options = _.defaults(options, { + fetchModel: false, + global: false + }); + + this.model = options.model; + + // Hide the widget if the scope has not permissions + if (!App.mv().validateInMetadata({ 'variables': [this.model.get('var_id')] })) { + return; + } + + this.className = this.model.get('className'); + }, + + + onClose: function () { + this.stopListening(); + }, + + render: function () { + var _this = this; + var metaData = App.Utils.toDeepJSON(App.mv().getVariable(this.model.get('var_id'))); + + this.$el.html(this._template({ m: this.model.toJSON(), 'metaData': metaData, 'fetchModel': this.options.fetchModel })); + + this.$el.addClass(this.model.get('className')); + + if (!this.options.fetchModel) { + this._draw(metaData); + } else { + this.model.fetch({ + data: this.model.get('data'), + success: function (data) { + _this.model.set('var_value', data.get('value')); + _this._draw(metaData); + _this.$('.co_value .value').html(App.nbf(data.get('value')) + '' + metaData.units + '') + } + }); + } + + + return this; + }, + + _draw: function (metaData) { + var _this = this; + var chart = this.$('.chart'); + + chart.html(''); + + var varRange = { + 'min': parseFloat(this.options.global ? metaData.config.global_threshold[0] : metaData.var_thresholds[0]), + 'warning': parseFloat(this.options.global ? metaData.config.global_threshold[1] : metaData.var_thresholds[1]), + 'error': parseFloat(this.options.global ? metaData.config.global_threshold[2] : metaData.var_thresholds[2]), + 'max': parseFloat(this.options.global ? metaData.config.global_threshold[3] : metaData.var_thresholds[3]) + }; + + this.model.set('var_value', this.model.get('var_value') == 'null' ? varRange.min : parseFloat(this.model.get('var_value'))); + + if (varRange['max'] < this.model.get('var_value')) + varRange['max'] = Math.ceil(this.model.get('var_value')); + + + if (varRange) { + if (metaData.reverse) { + if (this.model.get('var_value') <= varRange.error) { + this.$('.co_value .value').addClass('error'); + } else if (this.model.get('var_value') <= varRange.warning) { + this.$('.co_value .value').addClass('warning'); + } + + } else { + if (this.model.get('var_value') >= varRange.error) { + this.$('.co_value .value').addClass('error'); + } else if (this.model.get('var_value') >= varRange.warning) { + this.$('.co_value .value').addClass('warning'); + } + } + } + + var width = 210; + var height = 160; + var r = width / 2; + var ringWidth = 20; + var ringInset = 0; + var minAngle = -120; + var maxAngle = 120; + var minValue = varRange.min; + var maxValue = varRange.max; + var pointerWidth = 4; + var pointerTailLength = 0; + var pointerHeadLength = 81; + var labelFormat = d3.format(',g'); + var range = maxAngle - minAngle; + var scale = d3.scale.linear().range([0, 1]).domain([minValue, maxValue]); + var ticks = [varRange.min, varRange.warning, varRange.error, varRange.max]; + + var svg = d3.select(chart[0]) + .append('svg:svg') + .attr('class', 'gauge') + .attr('width', width) + .attr('height', height); + + var centerTx = 'translate(' + r + ',' + r + ')'; + + var arcs = svg.append('g') + .attr('class', 'arc') + .attr('transform', centerTx); + + var tickData = []; + tickData[0] = ((varRange.warning - varRange.min) / (varRange.max - varRange.min)) * range; + tickData[1] = tickData[0] + ((varRange.error - varRange.warning) / (varRange.max - varRange.min)) * range; + tickData[2] = tickData[1] + ((varRange.max - varRange.error) / (varRange.max - varRange.min)) * range; + + var arc = d3.svg.arc() + .innerRadius(r - ringWidth - ringInset) + .outerRadius(r - ringInset) + .startAngle(function (d, i) { + return _this._deg2rad(minAngle + (i == 0 ? 0 : tickData[i - 1])); + }) + .endAngle(function (d, i) { + return _this._deg2rad(minAngle + tickData[i]); + }) + ; + + + //TODO cambiar esto + if (metaData.reverse) { + arcs.selectAll('path') + .data(tickData) + .enter().append('path') + .attr('fill', function (d, i) { + if (i == 0) { + return App.Utils.rangeColor(App.Utils.RANGES.ERROR); + } else if (i == 1) { + return App.Utils.rangeColor(App.Utils.RANGES.WARNING); + } else if (i == 2) { + return App.Utils.rangeColor(App.Utils.RANGES.OK); + } + + }) + .attr('d', arc); + } else { + arcs.selectAll('path') + .data(tickData) + .enter().append('path') + .attr('fill', function (d, i) { + if (i == 0) { + return App.Utils.rangeColor(App.Utils.RANGES.OK); + } else if (i == 1) { + return App.Utils.rangeColor(App.Utils.RANGES.WARNING); + } else if (i == 2) { + return App.Utils.rangeColor(App.Utils.RANGES.ERROR); + } + + }) + .attr('d', arc); + } + + + ///////////////////////////////////////////////////////////////////////////////////////// + + var lg = svg.append('g') + .attr('class', 'label') + .attr('transform', centerTx); + + lg.selectAll('text') + .data(ticks) + .enter().append('text') + .attr('transform', function (d) { + var angle = (minAngle + (scale(d) * range)) - minAngle + (minAngle + 90) + var x = Math.cos(_this._deg2rad(angle)) * (-r) * 0.64; + var y = (Math.sin(_this._deg2rad(angle)) * (-r) * 0.64) + 5; + return 'translate(' + x + ',' + y + ')'; + }) + .attr("text-anchor", function (d) { + var angle = (minAngle + (scale(d) * range)) - minAngle + (minAngle + 90) + var anchor = ''; + angle > 90 ? anchor = 'end' : (angle == 90 ? anchor = 'middle' : anchor = 'start'); + return anchor; + }) + .text(labelFormat) + ; + + var lines = svg.append('g').attr('transform', centerTx); + for (var i = varRange.min; i <= varRange.max; i = i + ((varRange.max - varRange.min) / 30)) { + + var angle = (minAngle + (scale(i) * range)) - minAngle + (minAngle + 90) + var x = Math.cos(_this._deg2rad(angle)) * (-r) * 0.77; + var y = Math.sin(_this._deg2rad(angle)) * (-r) * 0.77; + + lines.append('line') + .attr("x1", x) + .attr("y1", y) + .attr("x2", x * 0.9) + .attr("y2", y * 0.9) + .style("stroke-width", "1px"); + }; + + + var lineData = [[pointerWidth / 2, 0], + [0, -pointerHeadLength], + [-(pointerWidth / 2), 0], + [0, pointerTailLength], + [pointerWidth / 2, 0]]; + + var pointerLine = d3.svg.line().interpolate('monotone'); + + var pg = svg.append('g').data([lineData]) + .attr('class', 'pointer') + .attr('transform', centerTx); + + // var pointer = pg.append('path') + // .attr('d', pointerLine) + // .attr('transform', 'rotate(' + minAngle +')'); + + svg.append("circle") + .attr("cx", r) + .attr("cy", r) + .attr("r", 5) + .style("fill", "#00475d"); + + var arc = d3.svg.arc() + .innerRadius(5) + .outerRadius(3) + .startAngle(0) + .endAngle(360); + + svg.append("path") + .attr("d", arc) + .attr('transform', 'translate(' + r + ',' + r + ')') + .attr('fill', '#fff'); + + if (this.model.get('var_value') != null && this.model.get('var_value') != undefined) { + + var pointer = pg.append('path') + .attr('d', pointerLine) + .attr('transform', 'rotate(' + minAngle + ')'); + + var newValue = this.model.get('var_value'); + var ratio = scale(newValue); + if (ratio < 0) { + ratio = -0.01; + } else if (ratio > 1) { + ratio = 1.01; + } + var newAngle = minAngle + (ratio * range); + pointer.attr('transform', 'rotate(-110)') + .transition() + .duration(2000) + .ease('elastic') + .attr('transform', 'rotate(' + newAngle + ')'); + } + }, + + _deg2rad: function (deg) { + return deg * Math.PI / 180; + } }); diff --git a/src/js/View/widgets/WidgetMultiVariableChart.js b/src/js/View/widgets/WidgetMultiVariableChart.js index 027b64e7..70dba34a 100644 --- a/src/js/View/widgets/WidgetMultiVariableChart.js +++ b/src/js/View/widgets/WidgetMultiVariableChart.js @@ -26,16 +26,13 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ _popup_template: _.template($('#chart-base_charttooltip').html()), _list_variable_template: _.template($('#widgets-widget_multiVariable_list_variables').html()), - // Size label in X axis - _sizeXLabel: 68, - /* TODO: Create documentation Params: el: DOM element (Optional) collection: Backbone Collection (Mandatory) Collection containing the chart data stepModel: Backbone Model (Optional) Model containing the current step - multiVariableModel: Backbone Model (Optional) Model containing a set of config + multiVariableModel: Backbone Model (Optional) Model containing a set of config parameters such as 'category' (string), 'title' (string) or 'aggDefaultValues' (JS Object) noAgg: true|false (Optional, default: false) Disables aggregation controls */ @@ -43,8 +40,10 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ this._stepModel = options.stepModel; this.collection = options.collection; this._multiVariableModel = _.defaults(options.multiVariableModel.toJSON() || {}, { + aggDefaultValues: [], + normalized: true, sizeDiff: 'days', - aggDefaultValues: [] + yAxisLabelDefault: null }); this.options = { noAgg: options.noAgg || false @@ -53,16 +52,28 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ ? this._multiVariableModel.aggDefaultValues : []; + // Use to block the rest request when any + // date request in working + this._lockDateRequest = false; + if (this._stepModel) { this.collection.options.step = this._stepModel.get('step'); - this.listenTo(this._stepModel, 'change:step', function () { - var regex = /\dd/; - this._multiVariableModel.sizeDiff = regex.test(this._stepModel.get('step')) - ? 'days' - : 'hours'; - this.collection.fetch({ 'reset': true }); - this.render(); - }); + this.listenTo(this._stepModel, 'change:step', _.debounce(function () { + + if (!this._lockDateRequest) { + var regex = /\dd/; + + this._multiVariableModel.sizeDiff = regex.test(this._stepModel.get('step')) + ? 'days' + : 'hours'; + this.collection.fetch({ + reset: true, + }); + + this.render(); + } + + }.bind(this), 250, true)); } this.collection.options.agg = this._aggDefaultValues @@ -79,28 +90,57 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ this.collection.fetch({ 'reset': true, data: this.collection.options.data || {} }) this._ctx = App.ctx; - this.listenTo(this._ctx, 'change:start change:finish change:bbox', function () { + this.listenTo(this._ctx, 'change:start change:finish change:bbox', + _.debounce(function () { + + if (!this._lockDateRequest) { + // Block the rest of requests + this._lockDateRequest = true; + + // Fix the changes in models and collections (BaseModel & BaseCollections) + if (this.collection + && this.collection.options + && typeof this.collection.options.data === 'string') { + this.collection.options.data = JSON.parse(this.collection.options.data); + } - // Fix the changes in models and collections (BaseModel & BaseCollections) - if (this.collection - && this.collection.options - && typeof this.collection.options.data === 'string') { - this.collection.options.data = JSON.parse(this.collection.options.data); - } + if (!this.collection.options.data) { + this.collection.options.data = { time: {} } + } + this.collection.options.data.time.start = this._ctx.getDateRange().start; + this.collection.options.data.time.finish = this._ctx.getDateRange().finish; + + // Set update step + App.Utils.checkBeforeFetching(this); + var currentStep = this._stepModel && this._stepModel.has('step') + ? this._stepModel.get('step') + : this.collection.options && + this.collection.options.data && + this.collection.options.data.step + ? this.collection.options.data.step + : this.collection.options.step || '1d'; + var regex = /\dd/; + this._multiVariableModel.sizeDiff = regex.test(currentStep) + ? 'days' + : 'hours'; + + this.collection.options.data.time.step = currentStep; + + // Launch request + this.collection.fetch({ + reset: true, + data: this.collection.options.data || {}, + success: function () { + this._lockDateRequest = false; // UnBlock the rest of requests + }.bind(this) + }) + // Render + this.render(); - if (!this.collection.options.data) { - this.collection.options.data = { time: {} } - } - this.collection.options.data.time.start = this._ctx.get('start'); - this.collection.options.data.time.finish = this._ctx.get('finish'); + } - App.Utils.checkBeforeFetching(this); - // Launch request - this.collection.fetch({ 'reset': true, data: this.collection.options.data || {} }) - // Render - this.render(); - }); + }.bind(this), 250, true)); this.render(); }, @@ -128,9 +168,15 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ var currentAggs = this._internalData.currentAggs; var agg = $(e.currentTarget).attr('data-agg'); + currentAggs[realKey] = agg; + // It works with the collection "DeviceTimeSerieChart" this.collection.options.agg[realKey] = agg; + // It works with the collection "TimeSeries" + this.collection.options.data.agg = []; + _.each(this.collection.options.vars, function (value) { + this.collection.options.data.agg.push(currentAggs[value]); + }.bind(this)); - currentAggs[realKey] = agg; this.collection.fetch({ 'reset': true, data: this.collection.options.data || {} }) this.render(); }, @@ -145,7 +191,11 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ var step = $(e.currentTarget).attr('data-step'); this.collection.options.step = step; this._stepModel.set('step', step); - this.collection.fetch({ 'reset': true, data: this.collection.options.data || {} }) + var data = typeof this.collection.options.data === 'string' ? JSON.parse(this.collection.options.data) : this.collection.options.data; + if (data.time) { + data.time.step = step; + } + this.collection.fetch({ 'reset': true, data: data || {} }) this.render(); }, @@ -159,16 +209,17 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ var tags = this.$('.btnLegend').size(); var realKey = $(event.target).closest('div').attr('id'); var disabledList = this._internalData.disabledList; + var variable = $(event.target).data('key'); - if (((disabledList[realKey] == undefined || disabledList[realKey] === false) && - this._internalData.elementsDisabled != tags - 1) || disabledList[realKey] === true) { + if (((typeof disabledList[realKey] === 'undefined' || disabledList[realKey] === false) && + this._internalData.elementsDisabled !== tags - 1) || disabledList[realKey] === true) { $($($('.chart .nv-series')).get($(event.target).parent().attr('tag'))).click(); $(event.target).parent().toggleClass('inactive'); disabledList[realKey] = !disabledList[realKey]; var $aggMenu = $(event.target).closest('div').find('a'); - if ($aggMenu.css('visibility') == 'hidden') { + if ($aggMenu.css('visibility') === 'hidden') { $aggMenu.css('visibility', 'visible'); } else { $aggMenu.css('visibility', 'hidden'); @@ -178,25 +229,29 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ ? this._internalData.elementsDisabled + 1 : this._internalData.elementsDisabled - 1; - var variable = $(event.target).text(); + // Change attribute "disabled" + var dataVariable = this.data.findWhere({ key: variable }); + var collectionVariable = this.collection.findWhere({ key: realKey }); - var model = this.data.findWhere({ key: variable }); - if (model != undefined) { - model.set('disabled', !model.get('disabled')); - } - var model = this.collection.findWhere({ key: realKey }); - if (model != undefined) { - model.set('disabled', !model.get('disabled')); + if (typeof dataVariable !== 'undefined') { + dataVariable.set('disabled', !dataVariable.get('disabled')); } - if (this.data.where({ 'disabled': false }).length == 1) { - var json = this._getUniqueDataEnableToDraw(); - this.svgChart.datum(json).call(this.chart); - this.svgChart.classed('normalized', false); - this._drawYAxis(); - } else { - this.svgChart.datum(this.data.toJSON()).call(this.chart) - this.svgChart.classed('normalized', true) + + if (typeof collectionVariable !== 'undefined') { + collectionVariable.set('disabled', !collectionVariable.get('disabled')); } + + var chartData = this.data.where({ 'disabled': false }).length === 1 + ? _.bind(this._getUniqueDataEnableToDraw, this) + : this.data.toJSON(); + + // Put the new data in chart + this.svgChart + .datum(chartData) + .call(this.chart) + + // Change Y Axis + this._drawYAxis(); } }, @@ -220,6 +275,7 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ */ _drawChart: function () { var oneVarInMultiVar = false; + // get initial step App.Utils.initStepData(this); // Hide the loading @@ -230,17 +286,21 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ // Set the keys (values) to 'disabled' to hide in the chart _.each(this._internalData.disabledList, function (value, key) { if (value) { - this.data.find({ 'realKey': key }).set('disabled', true); - this.collection.find({ 'key': key }).set('disabled', true); + this.data.find({ 'realKey': key }) + .set('disabled', true); + this.collection.find({ 'key': key }) + .set('disabled', true); } }.bind(this)); // Set 'normalized' CSS class if (this.data.where({ 'disabled': false }).length > 1) { - d3.select(this.$('.chart')[0]).classed('normalized', true); + d3.select(this.$('.chart')[0]) + .classed('normalized', true); } else { oneVarInMultiVar = true; - d3.select(this.$('.chart')[0]).classed('normalized', false); + d3.select(this.$('.chart')[0]) + .classed('normalized', false); } // Draw the chart with NVD3 @@ -252,7 +312,8 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ // Without data (CSS) if (this.data.length === 0) { - d3.select(this.$('.chart')[0]).classed('without-data', true); + d3.select(this.$('.chart')[0]) + .classed('without-data', true); } // Set margin legend @@ -281,6 +342,11 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ // draw the tooltip when we are on chart this._drawToolTip(); + // draw thresholds + if (this._multiVariableModel.yAxisThresholds) { + this._drawThresholds(); + } + // Update chart (redraw) this.chart.update(); // Update chart (redraw) when the window size changes @@ -289,10 +355,11 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ // Draw the keys list behind the chart this.$('.var_list').html( this._list_variable_template({ - colors: App.getSensorVariableColorList(), - data: this.data.toJSON(), + colors: this._multiVariableModel.colorsFn || App.getSensorVariableColorList(), currentAggs: this._internalData.currentAggs, - disabledList: this._internalData.disabledList + data: this.data.toJSON(), + disabledList: this._internalData.disabledList, + noAgg: this.options.noAgg }) ); @@ -303,7 +370,7 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ /** * Prepare the attribute "data" to the chart - * + * * @return {Array} - parse data */ _prepareDataToChart: function () { @@ -321,22 +388,32 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ _.each(this.collection.toJSON(), function (c, index) { if (parseData && parseData.length) { var data = parseData.findWhere({ 'realKey': c.key }); + if (data != undefined) { c.realKey = data.get('realKey'); c.key = data.get('key'); c.aggs = data.get('aggs'); c.currentAgg = data.get('currentAgg'); c.disabled = this._internalData.disabledList[c.realKey]; + if (this._multiVariableModel.colorsFn) { + c.color = this._multiVariableModel.colorsFn(c.realKey) + } this.collection.findWhere({ 'key': c.realKey }).set('disabled', c.disabled); } + } else { + c.realKey = c.key; c.key = App.mv().getVariable(c.key).get('name'); - c.aggs = App.mv().getVariable(c.realKey).get('var_agg'); + c.aggs = this._getAggregationsVariable(c.realKey); + + if (this._multiVariableModel.colorsFn) { + c.color = this._multiVariableModel.colorsFn(c.realKey) + } // TODO - DELETE AFTER AQUASIG PILOT JULY 2019 // Remove 'SUM' from variables (metadata) - if (c.realKey.indexOf('aq_cons.sensor') > -1) { + if (c.realKey.indexOf('aq_cons.sensor') > -1 && c.aggs.indexOf('SUM') > -1) { c.aggs.splice(c.aggs.indexOf('SUM'), 1); } // END TODO @@ -345,19 +422,22 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ var internalData = this._internalData; var meta = App.mv().getVariable(c.realKey); - if (meta && meta.get('config') && meta.get('config').hasOwnProperty('default')) { - internalData.disabledList[c.realKey] = !meta.get('config').default - } else { - internalData.disabledList[c.realKey] = false; + // To save the current status in the chart + if (typeof internalData.disabledList[c.realKey] === 'undefined') { + if (meta && meta.get('config') && meta.get('config').hasOwnProperty('default')) { + internalData.disabledList[c.realKey] = !meta.get('config').default + } else { + internalData.disabledList[c.realKey] = false; + } } if (!this.options.noAgg) { var currentDefaultAgg = !_.isEmpty(this._aggDefaultValues) ? this._aggDefaultValues[c.realKey] : null; - if ((c.aggs != undefined && c.aggs[0] != 'NOAGG') - && (_.isEmpty(this._aggDefaultValues) || (currentDefaultAgg != 'NONE'))) { - if (currentDefaultAgg == undefined || !_.contains(c.aggs, currentDefaultAgg.toUpperCase())) { + if ((typeof c.aggs !== undefined && Array.isArray(c.aggs) && !c.aggs.includes('NOAGG')) + && (_.isEmpty(this._aggDefaultValues) || (currentDefaultAgg !== 'NONE'))) { + if (typeof currentDefaultAgg === undefined || !_.contains(c.aggs, currentDefaultAgg.toUpperCase())) { c.currentAgg = c.aggs ? c.aggs[0] : null; this.collection.options.agg[c.realKey] = c.currentAgg; internalData.currentAggs[c.realKey] = c.currentAgg; @@ -384,13 +464,19 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ v.y = parseFloat(v.y); }) c.values = _.map(c.values, function (v) { + // Normalization the charts + var valueY = this._multiVariableModel.normalized + ? (max - min) > 0 + ? (v.y - min) / (max - min) + : 0 + : v.y; + return { x: v.x, - y: (max - min) > 0 - ? (v.y - min) / (max - min) - : 0, 'realY': v.y + y: valueY, + realY: v.y } - }); + }.bind(this)); }.bind(this)) ); @@ -402,11 +488,34 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ */ _getUniqueDataEnableToDraw: function () { return _.map(this.collection.toJSON(), function (j) { + j.realKey = j.key; j.key = this.data.findWhere({ 'realKey': j.key }).get('key'); + if (this._multiVariableModel.colorsFn) { + j.color = this._multiVariableModel.colorsFn(j.realKey) + } return j; }.bind(this)); }, + /** + * Get the aggregations variable + * + * @return {Array} aggregation + */ + _getAggregationsVariable: function (variable) { + var orderAggregations = ['SUM', 'MAX', 'AVG', 'MIN']; + var currentAggregations = App.mv().getVariable(variable).get('var_agg'); + + return _.filter(orderAggregations, function (orderAgg) { + if (_.find(currentAggregations, function (currentAgg) { + return orderAgg === currentAgg; + })) { + return true; + } + return false; + }); + }, + /** * Draw the X values in the chart */ @@ -415,19 +524,22 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ var dataChart = this.data.toJSON(); var startDate = dataChart[0].values[0].x; var finishDate = dataChart[0].values[dataChart[0].values.length - 1].x; - var fnRemoveXAxis = _.debounce(this._removeLabelInXAxis.bind(this), 350); + var fnHideMaxMinXAxis = _.debounce(_.bind(this.hideMaxMinXAxis, this), 350); // Draw the X axis with values (date) with 'hours' // If the difference between dates is minus to two days this.chart .xAxis + .showMaxMin(false) .tickFormat(function (d) { var localdate = moment.utc(d).local().toDate(); // Same day if (moment(finishDate).isSame(moment(startDate), 'day')) { return d3.time.format('%H:%M')(localdate); - } else if (this._multiVariableModel.sizeDiff === 'hours') { + } else if ((this._multiVariableModel.sizeDiff === 'hours' + || this._multiVariableModel.sizeDiff === 'minutes') + && moment(finishDate).diff(startDate, 'days') >= 1) { return d3.time.format('%d/%m - %H:%M')(localdate); } // Only date @@ -441,13 +553,13 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ // Recalculate the items in X axis this.chart .xAxis + .showMaxMin(false) .tickValues(this.getXTickValues(dataChart)); - // remove labels in X Axis - fnRemoveXAxis(); + // Remove max and min + fnHideMaxMinXAxis(); }.bind(this)); - - // remove labels in X Axis - fnRemoveXAxis(); + // Remove max and min + fnHideMaxMinXAxis(); } }, @@ -468,6 +580,7 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ return currentValue.realY < 1; }) ? App.d3Format.numberFormat(',.3r') : App.nbf; + // Put the label in Y Axis this.chart .yAxis .axisLabel( @@ -475,23 +588,117 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ (metadata.units ? ' (' + metadata.units + ')' : '') ); + // Put the values in Y Axis this.chart .yAxis .showMaxMin(false) - .tickFormat(this._multiVariableModel.yAxisFunction - ? this._multiVariableModel.yAxisFunction - : format + .tickFormat( + this._multiVariableModel.yAxisFunction + ? this._multiVariableModel.yAxisFunction + : format ); - this.svgChart.selectAll('.nv-focus .nv-y').call(this.chart.yAxis); + } else { + // Show the default Y axis label + this.chart + .yAxis + .axisLabel(this._multiVariableModel.yAxisLabelDefault || ''); + + if (this._multiVariableModel.normalized) { + // Clean the point (values) in Y Axis + this.chart + .yAxis + .showMaxMin(false) + .tickFormat(function () { + return '' + }); + } + } + + // The changes will be applied to the Y Axis + this.svgChart + .selectAll('.nv-focus .nv-y') + .call(this.chart.yAxis); + + if (this._multiVariableModel.yAxisThresholds) { + d3.selectAll(this.$('.chart > .nvd3 .nv-focus .nv-y > .nv-axis > g > g.tick:not(.zero)')).attr({ class: 'invisible' }).style({ opacity: 0 }); } + // Force y axis domain - if (this._multiVariableModel.yAxisDomain && - this._multiVariableModel.yAxisDomain[realKey]) { - this.chart.forceY(this._multiVariableModel.yAxisDomain[realKey]); + if (this._multiVariableModel.normalized) { + if (this._multiVariableModel.yAxisDomain && + this._multiVariableModel.yAxisDomain[realKey]) { + this.chart.forceY(this._multiVariableModel.yAxisDomain[realKey]); + } + } else { + if (this._multiVariableModel.yAxisDomain) { + this.chart.forceY(this._multiVariableModel.yAxisDomain); + } } }, + _drawThresholds: function () { + var _this = this; + this.chart.dispatch.on('renderEnd', function () { + d3.selectAll(_this.$('.chart > .nvd3 .nv-focus .nv-y > .nv-axis > g > g.tick:not(.zero)')).attr({ class: 'invisible' }).style({ opacity: 0 }); + + if (!d3.selectAll(_this.$('.chart > .nvd3 .nv-focus .th-groups')).empty()) { + return; + } + var chartRect = d3 + .selectAll(_this.$('.chart > .nvd3 .nv-focus')); + var g = chartRect.append('g').attr({ class: 'th-groups' }); + const lastDate = _this.data.models[0].get('values')[_this.data.models[0].get('values').length - 1].x + _this._multiVariableModel.yAxisThresholds.forEach(threshold => { + var thresholdGroup = g.append('g').attr({ class: 'thresholdGroup' }); + var height = _this.chart.yScale()(threshold.startValue) - _this.chart.yScale()(threshold.endValue); + var width = parseInt(d3.select(_this.$('.chart .nvd3 .nv-focus .nv-background rect')[0])[0][0].getAttribute('width'), 10); + + thresholdGroup.append('line').attr('class', 'thresholds') + .attr({ + x1: 0, + x2: width, + y1: _this.chart.yScale()(threshold.startValue), + y2: _this.chart.yScale()(threshold.startValue), + 'stroke-dasharray': 4, + stroke: threshold.color + }); + + thresholdGroup.append('rect') + .attr('class', 'thresholds') + .attr({ + x: 0, + y: _this.chart.yScale()(threshold.endValue), + width: width, + height: height, + fill: threshold.color, + 'fill-opacity': 0.1 + }); + + thresholdGroup.append('text') + .text(__(threshold.realName)) + .attr('class', 'axis-label') + .attr('x', 10) + .attr('y', _this.chart.yScale()(threshold.endValue) + height / 2) + .attr('dy', '.32em') + .attr('width', width) + .attr('height', height / 2) + .attr('class', 'thresholdLabel') + ; + + thresholdGroup.append('text') + .text(__(threshold.endValue)) + .attr('class', 'axis-label') + .attr('x', -15) + .attr('y', _this.chart.yScale()(threshold.endValue)) + .attr('dy', '.32em') + .attr('width', width) + .attr('class', 'thresholdValue') + ; + }); + }) + }, + /** * Set tooltip when we hover the chart (points) */ @@ -532,36 +739,95 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ /** * Get the values to use in X Axis (only work with times in X axis) - * + * * @param {Array} data - data to draw in chart * @return {Array} data to draw in chart */ getXTickValues: function (data) { if (data.length && data[0].values.length) { + // date start + var dateStart = data[0].values[0].x; + // date finish + var dateFinish = data[0].values[data[0].values.length - 1].x; + // date current (is used to the loop) + var dateCurrent = moment(dateStart); + // dates for X axis + var datesXAxis = [dateStart]; + // current step + var currentStep = this.collection.options && this.collection.options.step + ? this.collection.options.step + : '1d'; + // Values to step + var matchStep = /(\d+)(\w+)/g.exec(currentStep); + // Defaults values to the step + var stepValue = matchStep[1] || 1; + var stepRange = matchStep[2] || 'd'; + var ranges = { + d: 'days', + h: 'hours', + m: 'minutes' + }; + + // We fill (with dates) the period + while (dateCurrent.isBefore(dateFinish)) { + dateCurrent = dateCurrent.add(stepValue, ranges[stepRange]); + datesXAxis.push(dateCurrent.toDate()); + } + // chart DOM - var chart = d3.select(this.$('.chart')[0]); + var chartRect = d3 + .select(this.$('.chart .nvd3 .nv-focus .nv-background rect')[0]); // chart width DOM - var chartWidth = $(chart[0]).width(); + var chartRectWidth = Number + .parseInt(chartRect[0][0].getAttribute('width'), 10); // size label pixels put into the X axis - var labelWidth = this._sizeXLabel; + var labelWidth = ranges[stepRange] === 'days' + ? 62 //dates (59.34) + : labelWidth = (ranges[stepRange] === 'hours' || ranges[stepRange] === 'minutes') + && moment(dateFinish).diff(dateStart, 'days') >= 1 + ? 70 // dates + hours (67.17) + : 32 // hours (29.77) // max tick to draw in X Axis - var maxXTick = Number.parseInt(chartWidth / labelWidth, 10); + var maxXTick = Number.parseInt((chartRectWidth - (labelWidth / 2)) / labelWidth, 10); + // get multiples total dateXAxis + var multiplesTotalXAxis = App.Utils.getMultipleNumbers(datesXAxis.length); + + // If there are more the 2 values + // then we can search for the current multiple + if (multiplesTotalXAxis.length > 2) { + var newMaxTick = maxXTick; + for (var i = 0; i < multiplesTotalXAxis.length; i++) { + if (multiplesTotalXAxis[i] < maxXTick) { + newMaxTick = multiplesTotalXAxis[i]; + } + } + // Only change the maXTick value is newMaxTick + // is almost the half maXTick + if (newMaxTick >= maxXTick / 2) { + maxXTick = newMaxTick; + } + } + // Difference between the data to draw - var diff = data[0].values.length / maxXTick; + var diff = Math.round(datesXAxis.length / maxXTick); + + // Fix some (particulary) errors + if (datesXAxis.length > maxXTick && diff >= 1 && diff < 2) { + diff += 1; + maxXTick = Math.round(datesXAxis.length / diff); + } else if (maxXTick > 14) { + diff += 1; + maxXTick = Math.round(datesXAxis.length / diff); + } return diff < 1 - ? _.map(data[0].values, function (item) { - return item.x; + ? _.map(datesXAxis, function (item) { + return item; }) - : _.reduce(data[0].values, function (sumItems, item, index, originItems) { + : _.reduce(datesXAxis, function (sumItems, item, index, originItems) { var currentIndex = Math.round(index * diff); - if (sumItems.length <= maxXTick) { - if (index === 0 || (index + 1) === originItems.length) { - // Initial and finish range - sumItems.push(originItems[index].x); - } else if (originItems[currentIndex]) { - sumItems.push(originItems[currentIndex].x); - } + if (sumItems.length < maxXTick && originItems[currentIndex]) { + sumItems.push(originItems[currentIndex]); } return sumItems; }.bind(this), []); @@ -570,45 +836,15 @@ App.View.Widgets.MultiVariableChart = Backbone.View.extend({ } }, - // Remove label in X Axis - _removeLabelInXAxis: function () { - // Get all X axis points and remove (hide) any label - // that is over other label - var labels = this.$('.chart .nv-lineChart .nv-focus .nv-x .nv-axis > g:first-child g.tick') - .sort(function (a, b) { - // Order by transform valur - var aTransform = $(a).attr('transform'); - var bTransform = $(b).attr('transform'); - - aTransform = aTransform.replace('translate(', ''); - aTransform = aTransform.replace(',0)', ''); - aTransform = Number.parseFloat(aTransform); - - bTransform = bTransform.replace('translate(', ''); - bTransform = bTransform.replace(',0)', ''); - bTransform = Number.parseFloat(bTransform); - - if (aTransform < bTransform) { - return -1; - } else { - return 1; - } - }); + /** + * Remove max and min value in X axis + */ + hideMaxMinXAxis: function () { + var axisChart = + d3.selectAll(this.$('.chart .nvd3 .nv-focus .nv-axisMaxMin-x')); - var offsetXLabel = null; - _.each(labels, function (label) { - var currentOffsetXLabel = $(label).attr('transform'); - currentOffsetXLabel = currentOffsetXLabel.replace('translate(', ''); - currentOffsetXLabel = currentOffsetXLabel.replace(',0)', ''); - currentOffsetXLabel = Number.parseFloat(currentOffsetXLabel); - if (offsetXLabel === null) { // begin loop - offsetXLabel = currentOffsetXLabel; - } else if (offsetXLabel + this._sizeXLabel > currentOffsetXLabel) { // hide label - $(label).hide(); - } else { // show label - offsetXLabel = currentOffsetXLabel; - } - }.bind(this)); + $(axisChart[0][0]).hide(); + $(axisChart[0][1]).hide(); }, /** diff --git a/src/js/View/widgets/WidgetTable.js b/src/js/View/widgets/WidgetTable.js index 77ef56b5..bd5b797e 100644 --- a/src/js/View/widgets/WidgetTable.js +++ b/src/js/View/widgets/WidgetTable.js @@ -46,7 +46,7 @@ App.View.Widgets.Table = Backbone.View.extend({ this._template = options['template']; } - this._tableToCsv = new App.Collection.TableToCsv() + this._tableToCsv = new App.Collection.TableToCsv(); this._tableToCsv.url = this.collection.url; this._tableToCsv.fetch = this.collection.fetch; @@ -97,13 +97,23 @@ App.View.Widgets.Table = Backbone.View.extend({ }, _downloadCsv: function () { - this._tableToCsv.options = App.Utils.toDeepJSON(this.collection.options); - this._tableToCsv.options.format = 'csv'; + if (!this._tableToCsv.options.data) { + // We sure that there are some "data" + this._tableToCsv.options.data = {}; + } else if (typeof this._tableToCsv.options.data === 'string') { + // We sure that the "data" is an Object + this._tableToCsv.options.data = JSON.parse(this._tableToCsv.options.data); + } - this._tableToCsv.options.reset = false; - this._tableToCsv.options.dataType = 'text' + // Merge the "collection" options with "csv" options + this._tableToCsv.options = _.extend({}, this._tableToCsv.options, this.collection.options); + + // Add the neccesary attributes to "data" + this._tableToCsv.options.data = _.extend({}, this._tableToCsv.options.data, { + format: this._tableToCsv.options.format, + data_tz: this._tableToCsv.options.data_tz + }) - // this._tableToCsv.fetch({'reset':false,'dataType':'text'}) this._tableToCsv.fetch(this._tableToCsv.options); }, diff --git a/src/js/template/categories_list_template.html b/src/js/template/categories_list_template.html index 7ed711b8..56b902d3 100644 --- a/src/js/template/categories_list_template.html +++ b/src/js/template/categories_list_template.html @@ -15,7 +15,7 @@