diff --git a/webapp/core/bower.json b/webapp/core/bower.json index 5ada3de631..cfa58aa1fb 100644 --- a/webapp/core/bower.json +++ b/webapp/core/bower.json @@ -54,6 +54,7 @@ "datatables.net-fixedcolumns-bs": "3.2.2", "hp-autonomy-about-page": "0.3.0", "bootstrap": "3.3.7", + "Flot": "flot#~0.8.3", "moment-timezone": "0.5.11" }, "devDependencies": { diff --git a/webapp/core/src/main/less/app-include.less b/webapp/core/src/main/less/app-include.less index 5e0d931cc5..c4ed6ac848 100644 --- a/webapp/core/src/main/less/app-include.less +++ b/webapp/core/src/main/less/app-include.less @@ -1599,12 +1599,18 @@ input.find-input { width: @custom3; } -.parametric-value-count { +.parametric-value-graph-cell { + width: @custom3; + text-align: right; +} + +.parametric-value-count,.parametric-value-graph { &:extend(.text-right); } .parametric-value-name, -.parametric-value-count { +.parametric-value-count, +.parametric-value-graph{ white-space: nowrap; } @@ -2238,19 +2244,23 @@ h4.similar-dates-message { } @media @smHeightScreen { - .entity-topic-map { + .entity-topic-map, .dategraph-content { height: @custom4; } } @media @mHeightScreen { - .entity-topic-map { + .entity-topic-map, .dategraph-content { height: 500px; } } @media @lgHeightScreen { - .entity-topic-map { + .entity-topic-map, .dategraph-content { height: @lgHeightScreenTopicMapHeight; } } + +.dategraph-content { + clear: both; +} \ No newline at end of file diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/filter-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/filter-view.js index e648e7e19e..b3a29962d9 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/filter-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/filter-view.js @@ -35,8 +35,10 @@ define([ const IndexesView = options.IndexesView; this.collapsed = {}; + const config = configuration(); + const views = [{ - shown: configuration().enableMetaFilter, + shown: config.enableMetaFilter, initialize: function () { //Initializing the text with empty string to stop IE11 issue with triggering input event on render this.filterModel = new Backbone.Model({text: ''}); @@ -175,7 +177,8 @@ define([ inputTemplate: NumericParametricFieldView.dateInputTemplate, formatting: NumericParametricFieldView.dateFormatting, indexesCollection: options.indexesCollection, - filteredParametricCollection: filteredParametricCollection + filteredParametricCollection: filteredParametricCollection, + showGraphButtons: _.contains(config.resultViewOrder, 'dategraph') }); }.bind(this), get$els: function () { diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-field-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-field-view.js index 80cc71a38b..dfaaaa186c 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-field-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-field-view.js @@ -42,7 +42,8 @@ define([ queryModel: this.queryModel, parametricFieldsCollection: this.parametricFieldsCollection, selectedParametricValues: this.selectedParametricValues, - indexesCollection: this.indexesCollection + indexesCollection: this.indexesCollection, + showGraphButtons: this.showGraphButtons }); } }, @@ -52,6 +53,7 @@ define([ this.indexesCollection = options.indexesCollection; this.parametricFieldsCollection = options.parametricFieldsCollection; this.queryModel = options.queryModel; + this.showGraphButtons = options.showGraphButtons; this.listView = new ListView({ collection: this.collection, @@ -61,6 +63,7 @@ define([ tagName: 'tbody', itemOptions: { selectedValuesCollection: options.selectedValuesCollection, + showGraphButtons: options.showGraphButtons } }); }, @@ -109,7 +112,8 @@ define([ parametricFieldsCollection: this.parametricFieldsCollection, queryModel: this.queryModel, selectedParametricValues: this.selectedParametricValues, - indexesCollection: this.indexesCollection + indexesCollection: this.indexesCollection, + showGraphButtons: options.showGraphButtons }) }); diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-item-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-item-view.js index 15b28097a6..04158c9467 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-item-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-item-view.js @@ -15,12 +15,17 @@ define([ tagName: 'li', template: _.template(template), + initialize: function(options) { + this.showGraphButtons = options.showGraphButtons; + }, + render: function() { this.$el .html(this.template({ count: this.model.get('count') || 0, value: this.model.get('value'), - displayValue: this.model.get('displayValue') + displayValue: this.model.get('displayValue'), + showGraphButtons: this.showGraphButtons })) .iCheck({checkboxClass: 'icheckbox-hp'}); diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-list-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-list-view.js index 7e75f1ff9e..f17d04fc9e 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-list-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-list-view.js @@ -42,6 +42,9 @@ define([ ItemView: ItemView, collectionChangeEvents: { selected: 'updateSelected' + }, + itemOptions: { + showGraphButtons: options.showGraphButtons } }); diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-view.js index 8d2241a634..3c2f094443 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal-view.js @@ -57,7 +57,10 @@ define([ return { id: fieldModel.id, displayName: fieldModel.get('displayName'), - view: new ParametricSelectModalListView({paginator: paginator}) + view: new ParametricSelectModalListView({ + showGraphButtons: options.showGraphButtons, + paginator: paginator + }) }; }.bind(this)); @@ -102,6 +105,10 @@ define([ }); }, + getSelectedField: function(){ + return this.fieldSelectionModel.get('field'); + }, + remove: function () { this.fieldData.forEach(function (data) { data.view.remove(); diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal.js b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal.js index 3165a79daa..deeddae903 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-select-modal.js @@ -26,6 +26,10 @@ define([ 'shown.bs.modal': function() { // The content view will be visible now, so check if we need to load parametric values this.parametricSelectView.checkScroll(); + }, + 'click .parametric-value-graph': function(e){ + var $checkboxEl = $(e.currentTarget).prev() + this.externalSelectedValues.trigger('graph', this.parametricSelectView.getSelectedField(), $checkboxEl.data('value')); } }, Modal.prototype.events), @@ -40,7 +44,8 @@ define([ indexesCollection: options.indexesCollection, queryModel: options.queryModel, parametricFieldsCollection: options.parametricFieldsCollection, - selectedParametricValues: this.selectedParametricValues + selectedParametricValues: this.selectedParametricValues, + showGraphButtons: options.showGraphButtons }); Modal.prototype.initialize.call(this, { diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-value-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-value-view.js index 8a3a9b2cc0..69a4736350 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-value-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-value-view.js @@ -11,6 +11,7 @@ define([ initialize: function (options) { this.selectedValuesCollection = options.selectedValuesCollection; + this.showGraphButtons = options.showGraphButtons; this.$el.attr('data-value', this.model.get('value')); this.$el.attr('data-display-value', this.model.get('displayValue')); @@ -21,6 +22,10 @@ define([ render: function () { this.$el.html(template); + if (!this.showGraphButtons) { + this.$('.parametric-value-graph-cell').addClass('hide'); + } + this.$text = this.$('.parametric-value-text'); this.$name = this.$('.parametric-value-name'); this.$count = this.$('.parametric-value-count'); diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-view.js index 20012d6cf1..3f1d3759dd 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/filters/parametric/parametric-view.js @@ -41,7 +41,9 @@ define([ type: 'Parametric' }; - if (this.selectedParametricValues.get(attributes)) { + if ($(e.target).closest('.parametric-value-graph-cell').length) { + this.selectedParametricValues.trigger('graph', attributes.field, attributes.value); + } else if (this.selectedParametricValues.get(attributes)) { this.selectedParametricValues.remove(attributes); } else { this.selectedParametricValues.add(attributes); @@ -54,6 +56,7 @@ define([ this.filteredParametricCollection = options.filteredParametricCollection; this.selectedParametricValues = options.queryState.selectedParametricValues; this.filterModel = options.filterModel; + this.showGraphButtons = options.showGraphButtons; this.initializeProcessingBehaviour(); @@ -96,7 +99,8 @@ define([ parametricFieldsCollection: options.parametricFieldsCollection, filteredParametricCollection: this.filteredParametricCollection, selectedParametricValues: this.selectedParametricValues, - filterModel: this.filterModel + filterModel: this.filterModel, + showGraphButtons: options.showGraphButtons }, numericViewItemOptions: { inputTemplate: options.inputTemplate, @@ -110,7 +114,8 @@ define([ zoomEnabled: options.zoomEnabled, buttonsEnabled: options.buttonsEnabled, coordinatesEnabled: options.coordinatesEnabled, - collapsed: isCollapsed + collapsed: isCollapsed, + showGraphButtons: options.showGraphButtons } } }); diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/dategraph/dategraph-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/dategraph/dategraph-view.js new file mode 100644 index 0000000000..7637d6ed31 --- /dev/null +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/dategraph/dategraph-view.js @@ -0,0 +1,324 @@ +/* + * Copyright 2015 Hewlett-Packard Development Company, L.P. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + */ +define([ + 'backbone', + 'underscore', + 'd3', + 'find/app/util/topic-map-view', + 'find/app/model/entity-collection', + 'i18n!find/nls/bundle', + 'find/app/configuration', + 'find/app/page/search/filters/parametric/calibrate-buckets', + 'find/app/page/search/results/field-selection-view', + 'find/app/model/bucketed-parametric-collection', + 'parametric-refinement/to-field-text-node', + 'find/app/util/generate-error-support-message', + 'text!find/templates/app/page/search/results/dategraph/dategraph-view.html', + 'text!find/templates/app/page/loading-spinner.html', + 'iCheck', + 'slider/bootstrap-slider', + 'flot.time' +], function(Backbone, _, d3, TopicMapView, EntityCollection, i18n, configuration, calibrateBuckets, FieldSelectionView, + BucketedParametricCollection, toFieldTextNode, generateErrorHtml, template, + loadingTemplate) { + 'use strict'; + + var loadingHtml = _.template(loadingTemplate)({i18n: i18n, large: true}); + + var category10 = d3.scale.category10(); + + function rangeModelMatching(fieldName, dataType) { + var upperCase = fieldName.toUpperCase(); + + return function(model) { + return model.get('field').toUpperCase() === upperCase && model.get('range') && model.get('type') === dataType; + }; + } + + function dateModelMatching(fieldName, dataType) { + var upperCase = fieldName.toUpperCase(); + + return function(model) { + return model.id.toUpperCase() === upperCase && model.get('type') === dataType; + }; + } + + return Backbone.View.extend({ + template: _.template(template), + + events: { + 'click .dategraph-view-pptx': function(evt){ + evt.preventDefault(); + + var modelBuckets = this.bucketModel.get('values'); + var rows = []; + var multiAxes = !this.hideMainPlot && this.plots.length > 1; + + if (!this.hideMainPlot) { + rows.push({ + color: '#00B388', + label: i18n['search.resultsView.dategraph.defaultSeriesLabel'], + secondaryAxis: false, + values: _.pluck(modelBuckets, 'count') + }) + } + + _.each(this.plots, function(plot){ + var label = plot.field.replace(/^.*\//, '').replace(/_/g, '\u00A0') + ': ' + plot.value; + + rows.push({ + color: category10(label), + label: label, + secondaryAxis: multiAxes, + values: _.pluck(plot.model.get('values'), 'count') + }) + }) + + var data = { + rows: rows, + timestamps: _.map(modelBuckets, function(a) { + return Math.round(0.5*(a.min+a.max)); + }) + } + + var $form = $('
'); + $form[0].data.value = JSON.stringify(data) + $form.appendTo(document.body).submit().remove() + } + }, + + initialize: function(options) { + this.queryState = options.queryState; + + this.queryModel = options.queryModel; + this.pixelsPerBucket = options.pixelsPerBucket || 20; + + this.fieldName = 'autn_date'; + this.dataType = 'NumericDate'; + + this.bucketModel = new BucketedParametricCollection.Model({id: this.fieldName}); + this.selectedParametricValues = options.queryState.selectedParametricValues; + this.parametricFieldsCollection = options.parametricFieldsCollection; + + this.listenTo(this.queryModel, 'change', this.fetchBuckets); + + this.listenTo(this.bucketModel, 'change:values request sync error', this.updateGraph); + + this.listenTo(this.selectedParametricValues, 'graph', this.graphRequest) + + this.plots = [] + }, + + graphRequest: function(field, value){ + if (!_.where(this.plots, { field: field, value: value }).length) { + var model = new BucketedParametricCollection.Model({id: this.fieldName}); + var subPlot = { field: field, value: value, model: model }; + this.plots.push(subPlot); + this.listenTo(model, 'change:values', this.updateGraph) + this.lastOtherSelectedValues && this.fetchSubPlot(subPlot) + } + }, + + update: function() { + if(this.$el.is(':visible')) { + this.updateGraph(); + } + }, + + updateGraph: function() { + if (this.$tooltip) { + this.$tooltip.hide() + } + + var hadError = this.bucketModel.error; + var fetching = this.bucketModel.fetching; + var modelBuckets = this.bucketModel.get('values'); + var noValues = !modelBuckets || !modelBuckets.length; + + this.$('.dategraph-view-error-message').toggleClass('hide', !hadError); + this.$('.dategraph-view-empty-text').toggleClass('hide', hadError || !noValues || fetching); + + var hideLoadingIndicator = hadError || !fetching; + this.$('.dategraph-loading').toggleClass('hide', hideLoadingIndicator); + + + var $contentEl = this.$('.dategraph-content'); + var width = $contentEl.width(); + + function transform(values) { + return values.map(function (a) { + return [0.5e3 * (a.min + a.max), a.count, a.min, a.max] + }) + } + + if(!hadError && !noValues && hideLoadingIndicator && width > 0) { + var multiAxes = !this.hideMainPlot && this.plots.length > 1; + + var data = (this.hideMainPlot ? [] : [{ + color: '#00B388', + label: i18n['search.resultsView.dategraph.defaultSeriesLabel'], + data: transform(modelBuckets) + }]).concat(_.map(this.plots, function(plot, idx, plots){ + var label = plot.field.replace(/^.*\//, '').replace(/_/g, '\u00A0') + ': ' + plot.value; + return { + color: category10(label), + label: label, + data: transform(plot.model.get('values')), + yaxis: multiAxes ? 2 : 1 + } + })) + + var yaxes = [{ minTickSize: 1 }]; + + if (multiAxes) { + yaxes.push({ minTickSize: 1, position: 'right' }); + } + + $.plot($contentEl[0], data, { + grid: { hoverable: true }, + xaxis: {mode: 'time'}, + yaxes: yaxes + }) + + this.$('.dategraph-view-pptx').removeClass('disabled'); + } + else { + $contentEl.empty(); + this.$('.dategraph-view-pptx').addClass('disabled'); + } + }, + + fetchBuckets: function() { + // We now use the width of the closest visible ancestor, even if this tab itself is currently hidden, + // since the filters may change when we're not looking at the screen; which gives zero width for + // the SVG if it's not on the screen, but we still need data. + var width = this.$('.dategraph-content').closest(':visible').width(); + + // If the SVG has no width or there are no values, there is no point fetching new data + // if(width !== 0 && this.model.get('totalValues') !== 0) { + if(width) { + var rangeFilter = this.selectedParametricValues.find(rangeModelMatching(this.fieldName, this.dataType)); + + var otherSelectedValues = this.selectedParametricValues + .map(function(model) { + return model.toJSON(); + }); + + var dateRange = rangeFilter && rangeFilter.get('range'); + + var minDate = this.queryModel.getIsoDate('minDate'); + var maxDate = this.queryModel.getIsoDate('maxDate'); + + var dateField = this.parametricFieldsCollection.find(dateModelMatching(this.fieldName, this.dataType)); + + var baseParams = { + queryText: this.queryModel.get('queryText'), + fieldText: toFieldTextNode(otherSelectedValues), + minDate: minDate, + maxDate: maxDate, + minScore: this.queryModel.get('minScore'), + databases: this.queryModel.get('indexes'), + targetNumberOfBuckets: Math.floor(width / this.pixelsPerBucket), + bucketMin: dateRange ? dateRange[0] : dateField ? dateField.get('min') : Math.floor((new Date().getTime() - 86400e3*365)/1000), + bucketMax: dateRange ? dateRange[1] : dateField ? dateField.get('max') : Math.floor(new Date().getTime()/1000) + }; + + this.lastBaseParams = baseParams; + this.lastOtherSelectedValues = otherSelectedValues; + + this.hideMainPlot = false; + + this.bucketModel.fetch({ + data: baseParams + }); + + _.each(this.plots, this.fetchSubPlot, this) + } + }, + + fetchSubPlot: function(plot){ + var newFieldText = toFieldTextNode([{field: plot.field, value: plot.value}]) + var plotSelectedValues = toFieldTextNode(this.lastOtherSelectedValues); + + plot.model.set([]) + plot.model.fetch({ + data: _.defaults({ + fieldText: plotSelectedValues ? '(' + plotSelectedValues + ') AND (' + newFieldText + ')': newFieldText + }, this.lastBaseParams) + }) + }, + + render: function() { + this.$el.html(this.template({ + i18n: i18n, + errorTemplate: this.errorTemplate, + loadingHtml: loadingHtml, + cid: this.cid + })); + + this.$('.dategraph-content').on('click .legendColorBox', _.bind(function(evt){ + var idx = $(evt.target).closest('tr').index(), removed + + if (idx >= 0) { + if (this.hideMainPlot) { + removed = this.plots.splice(idx, 1) + this.stopListening(removed[0].model) + + if (!this.plots.length) { + this.hideMainPlot = false; + } + + this.updateGraph(); + } + else if (idx) { + removed = this.plots.splice(idx - 1, 1) + this.stopListening(removed[0].model) + + this.updateGraph(); + } + else if (this.plots.length) { + this.hideMainPlot = true; + this.updateGraph(); + } + } + }, this)).on('plothover', _.bind(function(evt, pos, item){ + function lPad2(num) { + return num < 10 ? '0' + num : num; + } + + function formatDate(epochSeconds){ + var date = new Date(1000 * epochSeconds); + return [ + [date.getFullYear(), lPad2(date.getMonth() + 1), lPad2(date.getDate())].join('-'), + [lPad2(date.getHours()), lPad2(date.getMinutes())].join(':') + ] + } + + if (item) { + if (!this.$tooltip) { + this.$tooltip = $('