Commit 49b383fd by Zachary Tong

Allow binning on secondary field

parent e12f1312
...@@ -24,7 +24,9 @@ ...@@ -24,7 +24,9 @@
<div class="control-group"> <div class="control-group">
<label class="control-label" for="panelfield">Primary Field</label> <label class="control-label" for="panelfield">Primary Field</label>
<div class="controls"> <div class="controls">
<input type="text" id="panelfield" class="input" ng-model="panel.field"> <input type="text" id="panelfield" class="input"
ng-model="panel.field"
ng-change="get_data()" />
</div> </div>
</div> </div>
<div class="control-group"> <div class="control-group">
...@@ -32,6 +34,7 @@ ...@@ -32,6 +34,7 @@
<div class="controls"> <div class="controls">
<input type="text" id="panelsecondaryfield" class="input" <input type="text" id="panelsecondaryfield" class="input"
ng-model="panel.secondaryfield" ng-model="panel.secondaryfield"
ng-change="get_data()"
data-placement="right" data-placement="right"
placeholder="Optional" placeholder="Optional"
bs-tooltip="'Allows aggregating on Primary field, while counting stats on a secondary (e.g. Group By user_id, Sum(purchase_price)).'" /> bs-tooltip="'Allows aggregating on Primary field, while counting stats on a secondary (e.g. Group By user_id, Sum(purchase_price)).'" />
...@@ -149,12 +152,30 @@ ...@@ -149,12 +152,30 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Encoding</td>
<td> <td>
<div class="btn-group" ng-model="panel.display.binning.encoding" bs-buttons-radio ng-change="$emit('render')"> <button type="button" class="btn" bs-button
<button type="button" class="btn" value="areacolor" >Area + Color</button> ng-change="$emit('render')"
<button type="button" class="btn" value="area">Area</button> ng-class="{'btn-success': panel.display.binning.areaEncoding}"
<button type="button" class="btn" value="color">Color</button> ng-model="panel.display.binning.areaEncoding">Area</button>
</td>
<td>
<div class="btn-group" ng-model="panel.display.binning.areaEncodingField" bs-buttons-radio ng-change="$emit('render')">
<button type="button" class="btn" value="primary">Primary Field</button>
<button type="button" class="btn" value="secondary">Secondary Field</button>
</div>
</td>
</tr>
<tr>
<td>
<button type="button" class="btn" bs-button
ng-change="$emit('render')"
ng-class="{'btn-success': panel.display.binning.colorEncoding}"
ng-model="panel.display.binning.colorEncoding">Color</button>
</td>
<td>
<div class="btn-group" ng-model="panel.display.binning.colorEncodingField" bs-buttons-radio ng-change="$emit('render')">
<button type="button" class="btn" value="primary">Primary Field</button>
<button type="button" class="btn" value="secondary">Secondary Field</button>
</div> </div>
</td> </td>
</tr> </tr>
......
...@@ -25,7 +25,10 @@ angular.module('kibana.map2', []) ...@@ -25,7 +25,10 @@ angular.module('kibana.map2', [])
enabled: true, enabled: true,
hexagonSize: 10, hexagonSize: 10,
hexagonAlpha: 1.0, hexagonAlpha: 1.0,
encoding: "color" areaEncoding: true,
areaEncodingField: "primary",
colorEncoding: true,
colorEncodingField: "primary"
} }
}, },
displayTabs: ["Geopoints", "Binning", "Data"], displayTabs: ["Geopoints", "Binning", "Data"],
...@@ -63,16 +66,39 @@ angular.module('kibana.map2', []) ...@@ -63,16 +66,39 @@ angular.module('kibana.map2', [])
var request = $scope.ejs.Request().indices($scope.panel.index); var request = $scope.ejs.Request().indices($scope.panel.index);
var facet = $scope.ejs.TermsFacet('map') console.log("fields", $scope.panel.field, $scope.panel.secondaryfield);
.field($scope.panel.field)
.size($scope.panel.display.data.samples) //Use a regular term facet if there is no secondary field
.exclude($scope.panel.exclude) if (typeof $scope.panel.secondaryfield === "undefined") {
.facetFilter(ejs.QueryFilter( var facet = $scope.ejs.TermsFacet('map')
ejs.FilteredQuery( .field($scope.panel.field)
ejs.QueryStringQuery($scope.panel.query || '*'), .size($scope.panel.display.data.samples)
ejs.RangeFilter($scope.time.field) .exclude($scope.panel.exclude)
.from($scope.time.from) .facetFilter(ejs.QueryFilter(
.to($scope.time.to)))); ejs.FilteredQuery(
ejs.QueryStringQuery($scope.panel.query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to))));
} else {
//otherwise, use term stats
//NOTE: this will break if valueField is a geo_point
// need to put in checks for that
var facet = $scope.ejs.TermStatsFacet('map')
.keyField($scope.panel.field)
.valueField($scope.panel.secondaryfield)
.size($scope.panel.display.data.samples)
.facetFilter(ejs.QueryFilter(
ejs.FilteredQuery(
ejs.QueryStringQuery($scope.panel.query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to))));
}
// Then the insert into facet and make the request // Then the insert into facet and make the request
var request = request.facet(facet).size(0); var request = request.facet(facet).size(0);
...@@ -81,19 +107,31 @@ angular.module('kibana.map2', []) ...@@ -81,19 +107,31 @@ angular.module('kibana.map2', [])
var results = request.doSearch(); var results = request.doSearch();
// Populate scope when we have results // Populate scope when we have results
results.then(function (results) { results.then(function (results) {
$scope.panel.loading = false; $scope.panel.loading = false;
$scope.hits = results.hits.total; $scope.hits = results.hits.total;
$scope.data = {}; $scope.data = {};
_.each(results.facets.map.terms, function (v) { _.each(results.facets.map.terms, function (v) {
var metric = 'count';
//If it is a Term facet, use count, otherwise use Total
//May retool this to allow users to pick mean/median/etc
if (typeof $scope.panel.secondaryfield === "undefined") {
metric = 'count';
} else {
metric = 'total';
}
//FIX THIS //FIX THIS
if (!$scope.isNumber(v.term)) { if (!$scope.isNumber(v.term)) {
$scope.data[v.term.toUpperCase()] = v.count; $scope.data[v.term.toUpperCase()] = v[metric];
} else { } else {
$scope.data[v.term] = v.count; $scope.data[v.term] = v[metric];
} }
}); });
...@@ -133,7 +171,6 @@ angular.module('kibana.map2', []) ...@@ -133,7 +171,6 @@ angular.module('kibana.map2', [])
}) })
.filter('enabledText', function() { .filter('enabledText', function() {
return function (value) { return function (value) {
console.log(value);
if (value === true) { if (value === true) {
return "Enabled"; return "Enabled";
} else { } else {
...@@ -161,9 +198,9 @@ angular.module('kibana.map2', []) ...@@ -161,9 +198,9 @@ angular.module('kibana.map2', [])
}); });
function render_panel() { function render_panel() {
console.log("render_panel"); //console.log("render_panel");
console.log(scope.panel); //console.log(scope.panel);
console.log(elem); //console.log(elem);
// Using LABjs, wait until all scripts are loaded before rendering panel // Using LABjs, wait until all scripts are loaded before rendering panel
var scripts = $LAB.script("panels/map2/lib/d3.v3.min.js") var scripts = $LAB.script("panels/map2/lib/d3.v3.min.js")
...@@ -225,13 +262,40 @@ angular.module('kibana.map2', []) ...@@ -225,13 +262,40 @@ angular.module('kibana.map2', [])
.attr("d", path); .attr("d", path);
//Geocoded points are decoded into lat/lon, then projected onto x/y //Geocoded points are decoded into lat/lon, then projected onto x/y
var points = _.map(scope.data, function (k, v) { points = _.map(scope.data, function (k, v) {
var decoded = geohash.decode(v); var decoded = geohash.decode(v);
return projection([decoded.longitude, decoded.latitude]); return projection([decoded.longitude, decoded.latitude]);
}); });
var binPoints = [];
//primary field is just binning raw counts
//secondary field is binning some metric like mean/median/total. Hexbins doesn't support that,
//so we cheat a little and just add more points to compensate.
//However, we don't want to add a million points, so normalize against the largest value
if (scope.panel.display.binning.areaEncodingField === 'secondary') {
var max = Math.max.apply(Math, _.map(scope.data, function(k,v){return k;})),
scale = 10/max;
_.map(scope.data, function (k, v) {
var decoded = geohash.decode(v);
return _.map(_.range(0, k*scale), function(a,b) {
binPoints.push(projection([decoded.longitude, decoded.latitude]));
})
});
} else {
binPoints = points;
}
//hexagonal binning //hexagonal binning
if (scope.panel.display.binning.enabled) { if (scope.panel.display.binning.enabled) {
...@@ -240,7 +304,11 @@ angular.module('kibana.map2', []) ...@@ -240,7 +304,11 @@ angular.module('kibana.map2', [])
.radius(scope.panel.display.binning.hexagonSize); .radius(scope.panel.display.binning.hexagonSize);
//bin and sort the points, so we can set the various ranges appropriately //bin and sort the points, so we can set the various ranges appropriately
var binnedPoints = hexbin(points).sort(function(a, b) { return b.length - a.length; }); var binnedPoints = hexbin(binPoints).sort(function(a, b) { return b.length - a.length; });;
console.log(binnedPoints);
//clean up some memory
binPoints = [];
var radius = d3.scale.sqrt() var radius = d3.scale.sqrt()
.domain([0, binnedPoints[0].length]) .domain([0, binnedPoints[0].length])
...@@ -256,7 +324,7 @@ angular.module('kibana.map2', []) ...@@ -256,7 +324,7 @@ angular.module('kibana.map2', [])
.data(binnedPoints) .data(binnedPoints)
.enter().append("path") .enter().append("path")
.attr("d", function (d) { .attr("d", function (d) {
if (scope.panel.display.binning.encoding === 'color') { if (scope.panel.display.binning.areaEncoding === false) {
return hexbin.hexagon(); return hexbin.hexagon();
} else { } else {
return hexbin.hexagon(radius(d.length)); return hexbin.hexagon(radius(d.length));
...@@ -267,8 +335,8 @@ angular.module('kibana.map2', []) ...@@ -267,8 +335,8 @@ angular.module('kibana.map2', [])
return "translate(" + d.x + "," + d.y + ")"; return "translate(" + d.x + "," + d.y + ")";
}) })
.style("fill", function (d) { .style("fill", function (d) {
if (scope.panel.display.binning.encoding === 'area') { if (scope.panel.display.binning.colorEncoding === false) {
return color(10); return color(binnedPoints[0].length / 2);
} else { } else {
return color(d.length); return color(d.length);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment