浏览代码

[search] Dynamic range of historgram widget

Romain Rigaux 11 年之前
父节点
当前提交
8cd81eb

+ 16 - 4
apps/search/src/search/api.py

@@ -62,7 +62,8 @@ def _guess_range_facet(widget_type, solr_api, collection, facet_field, propertie
         stats_min = max(start, stats_min)
       if end is not None:
         stats_max = min(end, stats_max)   
-             
+      # TODO: check min is min of max + refactor
+      
       if gap is None:
         gap = (stats_max - stats_min) / SLOTS
       if gap < 1:
@@ -80,6 +81,7 @@ def _guess_range_facet(widget_type, solr_api, collection, facet_field, propertie
         unit = 'SECONDS'
       elif difference < 60:
         unit = 'MINUTES'
+        # todo 0, 5, 10, ...
       elif difference < 3600:
         unit = 'HOURS'
       elif difference < 3600 * 24:
@@ -106,10 +108,20 @@ def _guess_range_facet(widget_type, solr_api, collection, facet_field, propertie
 def _guess_gap(solr_api, collection, facet_field, start=None, end=None):
   properties = {}
   _guess_range_facet('range-widget', solr_api, collection, facet_field, properties, start=start, end=end)
-  return properties.get('gap')
+  return properties
+
+
+def _new_range_facet(solr_api, collection, facet_field):
+  properties = {}
+  _guess_range_facet('range-widget', solr_api, collection, facet_field, properties)
+  return properties
+
+
+def _zoom_range_facet(solr_api, collection, facet_field, direction='out'):
+  properties = {}
+  _guess_range_facet('range-widget', solr_api, collection, facet_field, properties)
+  return properties
 
-def _zoom_range_facet(facet, direction):
-  pass
 
 class SolrApi(object):
   """

+ 24 - 7
apps/search/src/search/templates/search2.mako

@@ -389,8 +389,8 @@ ${ commonheader(_('Search'), "search", user, "60px") | n,unicode }
       ${ _('Gap') }: <input type="text" data-bind="value: properties.gap" />
     </div>  
 
-    <a href="javascript:void(0)"><i class="fa fa-plus"></i></a>
-    <a href="javascript:void(0)"><i class="fa fa-minus"></i></a>
+    ##<a href="javascript:void(0)"><i class="fa fa-plus"></i></a>
+    <a href="javascript:void(0)" data-bind="click: $root.collection.timeLineZoom"><i class="fa fa-minus"></i></a>
 
     <div data-bind="timelineChart: {datum: {counts: counts, widget_id: $parent.id(), label: label}, field: field, label: label, transformer: timelineChartDataTransformer,
       onSelectRange: function(from, to){viewModel.collection.selectTimelineFacet({from: from, to: to, cat: field, widget_id: $parent.id()})},
@@ -440,12 +440,16 @@ ${ commonheader(_('Search'), "search", user, "60px") | n,unicode }
     </div>
     <div data-bind="with: $root.collection.getFacetById($parent.id())">
       <!-- ko if: type() == 'range' -->
-      <div data-bind="pieChart: {data: {counts: $parent.counts, widget_id: $parent.id}, transformer: rangePieChartDataTransformer,
-         onClick: function(d){ viewModel.query.selectRangeFacet({count: d.data.obj.value, widget_id: d.data.obj.widget_id, from: d.data.obj.from, to: d.data.obj.to, cat: d.data.obj.field}) }, 
-         onComplete: function(){ viewModel.getWidgetById($parent.id).isLoading(false)} }" />
+      <div data-bind="pieChart: {data: {counts: $parent.counts, widget_id: $parent.id}, field: field, fqs: $root.query.fqs,
+        transformer: rangePieChartDataTransformer,
+        onClick: function(d){ viewModel.query.selectRangeFacet({count: d.data.obj.value, widget_id: d.data.obj.widget_id, from: d.data.obj.from, to: d.data.obj.to, cat: d.data.obj.field}) }, 
+        onComplete: function(){ viewModel.getWidgetById($parent.id).isLoading(false)} }" />
       <!-- /ko -->
       <!-- ko if: type() != 'range' -->
-      <div data-bind="pieChart: {data: {counts: $parent.counts, widget_id: $parent.id}, field: field, fqs: $root.query.fqs, transformer: pieChartDataTransformer, onClick: function(d){viewModel.query.toggleFacet({facet: d.data.obj, widget_id: d.data.obj.widget_id})}, onComplete: function(){viewModel.getWidgetById($parent.id).isLoading(false)}}" />
+      <div data-bind="pieChart: {data: {counts: $parent.counts, widget_id: $parent.id}, field: field, fqs: $root.query.fqs,
+        transformer: pieChartDataTransformer,
+        onClick: function(d){viewModel.query.toggleFacet({facet: d.data.obj, widget_id: d.data.obj.widget_id})},
+        onComplete: function(){viewModel.getWidgetById($parent.id).isLoading(false)}}" />
       <!-- /ko -->
     </div>    
   </div>
@@ -811,10 +815,23 @@ function timelineChartDataTransformer(rawDatum) {
       obj: item
     });
   });
-  _datum.push({
+    _datum.push({
     key: rawDatum.label,
     values: _data
   });
+  var _data = [];
+  $(rawDatum.counts).each(function (cnt, item) {
+    _data.push({
+      series: 1,
+      x: new Date(moment(item.from).valueOf()),
+      y: item.value,
+      obj: item
+    });
+  });  
+    _datum.push({
+    key: rawDatum.label + " #2",
+    values: _data
+  });
   return _datum;
 }
 

+ 31 - 56
apps/search/src/search/views.py

@@ -17,10 +17,6 @@
 
 import json
 import logging
-import numbers
-
-from datetime import datetime
-from time import mktime
 
 from django.core.urlresolvers import reverse
 from django.http import HttpResponse
@@ -31,7 +27,7 @@ from django.shortcuts import redirect
 from desktop.lib.django_util import render
 from desktop.lib.exceptions_renderable import PopupException
 
-from search.api import SolrApi, _guess_gap
+from search.api import SolrApi, _guess_gap, _zoom_range_facet, _new_range_facet
 from search.conf import SOLR_URL
 from search.data_export import download as export_download
 from search.decorators import allow_admin_only
@@ -507,56 +503,13 @@ def new_facet(request):
       'sort': 'desc',
       'canRange': False,
     }
-    is_range = False
-
-    try:
-      if widget_type == 'pie-widget':
-        SLOTS = 5
-      elif widget_type == 'facet-widget':
-        SLOTS = 10
-      else:
-        SLOTS = 50
-      stats_json = SolrApi(SOLR_URL.get(), request.user).stats(collection['name'], [facet_field])
-      stat_facet = stats_json['stats']['stats_fields'][facet_field]
-      
-      if isinstance(stat_facet['min'], numbers.Number):
-        stats_min = int(stat_facet['min']) # if field is float, cast as float isinstance(y, float)
-        stats_max = int(stat_facet['max'])
-        gap = (stats_max - stats_min) / SLOTS
-        if gap < 1:
-          gap = 1
-        is_range = True
-      elif 'T' in stat_facet['min']:
-        stats_min = stat_facet['min']
-        stats_max = stat_facet['max']
-        difference = (
-            mktime(datetime.strptime(stats_max, '%Y-%m-%dT%H:%M:%SZ').timetuple()) - 
-            mktime(datetime.strptime(stats_min, '%Y-%m-%dT%H:%M:%SZ').timetuple())) / SLOTS
-  
-        if difference < 1:
-          unit = 'SECONDS'
-        elif difference < 60:
-          unit = 'MINUTES'
-        elif difference < 3600:
-          unit = 'HOURS'
-        elif difference < 3600 * 24:
-          unit = 'DAYS'
-        else:
-          unit = 'MONTHS'
-        gap = '+1' + unit      
-        is_range = True
-    except Exception, e:
-      # stats not supported on all the fields, like text
-      pass
-                
-    if is_range:
+    
+    solr_api = SolrApi(SOLR_URL.get(), request.user)
+    range_properties = _new_range_facet(solr_api, collection, facet_field)
+                          
+    if range_properties:
       facet_type = 'range'
-      properties.update({
-        'start': stats_min,
-        'end': stats_max,
-        'gap': gap,
-        'canRange': True,
-      })       
+      properties.update(range_properties)       
     elif widget_type == 'hit-widget':
       facet_type = 'query'      
     else:
@@ -577,9 +530,31 @@ def new_facet(request):
 
   return HttpResponse(json.dumps(result), mimetype="application/json")
 
+# TODO security
+def get_range_facet(request):  
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    collection = json.loads(request.POST.get('collection', '{}'))
+    facet = json.loads(request.POST.get('facet', '{}'))
+    action = request.POST.get('action', 'select')
+    
+            
+    solr_api = SolrApi(SOLR_URL.get(), request.user)
+    
+    if action == 'select':
+      properties = _guess_gap(solr_api, collection, facet['field'], facet['properties']['start'], facet['properties']['end'])
+    else:
+      properties = _zoom_range_facet(solr_api, collection, facet['field'])
+            
+    result['properties'] = properties
+    result['status'] = 0      
+
+  except Exception, e:
+    result['message'] = unicode(str(e), "utf8")
+
+  return HttpResponse(json.dumps(result), mimetype="application/json")
 
-def get_range_facet(request):
-  _guess_gap(self, collection, facet['field'], facet['properties']['start'], facet['properties']['end'])
 
 def install_examples(request):
   result = {'status': -1, 'message': ''}

+ 44 - 2
apps/search/static/js/search.ko.js

@@ -425,10 +425,52 @@ var Collection = function (vm, collection) {
 	
 	facet.properties.start(data.from);
 	facet.properties.end(data.to);
-	//facet.properties.gap(null);
+	
+	$.ajax({
+	  type: "POST",
+	  url: "/search/get_range_facet",	  
+	  data: {
+	    collection: ko.mapping.toJSON(self),
+	    facet: ko.mapping.toJSON(facet),
+	    action: 'select'
+	  },
+	  success: function (data) {
+	    if (data.status == 0) {
+	      facet.properties.gap(data.properties.gap);
+	    }
+	  },
+	  async: false
+	});	
+	
+    vm.search();
+  }
 
+  self.timeLineZoom = function (facet_json) { 
+	var facet = self.getFacetById(facet_json.id);
+	
+	facet.properties.start(facet.from);
+	facet.properties.end(facet.to);
+	
+	$.ajax({
+	  type: "POST",
+	  url: "/search/get_range_facet",	  
+	  data: {
+	    collection: ko.mapping.toJSON(self),
+	    facet: ko.mapping.toJSON(facet),
+	    action: "zoom_out"
+	  },
+	  success: function (data) {
+	    if (data.status == 0) {
+	      facet.properties.start(data.properties.start);
+	      facet.properties.end(data.properties.end);
+	      facet.properties.gap(data.properties.gap);
+	    }
+	  },
+	  async: false
+	});	
+	
     vm.search();
-  }    
+  }
   
   self.translateSelectedField = function (index, direction) {
 	var array = self.template.fieldsSelected();