Procházet zdrojové kódy

Merge pull request #53 from polyfractal/map2

New Map Panel
Zachary Tong před 12 roky
rodič
revize
547ca3cb10

+ 2 - 1
common/css/bootstrap.min.css

@@ -183,7 +183,8 @@ select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="chec
 .uneditable-textarea{width:auto;height:auto;}
 input:-moz-placeholder,textarea:-moz-placeholder{color:#4d4d4d;}
 input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#4d4d4d;}
-input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#4d4d4d;}
+input:-webkit-input-placeholder{color:#4d4d4d;}
+textarea:-webkit-input-placeholder{color:#4d4d4d;}
 .radio,.checkbox{min-height:20px;padding-left:20px;}
 .radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px;}
 .controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;}

+ 1 - 1
config.js

@@ -14,7 +14,7 @@ var config = new Settings(
 {
   elasticsearch:  'http://localhost:9200',
   kibana_index:   "kibana-int", 
-  modules:        ['histogram','map','pie','table','stringquery','sort',
+  modules:        ['histogram','map','map2','pie','table','stringquery','sort',
                   'timepicker','text','fields','hits','dashcontrol',
                   'column', 'parallelcoordinates'],
   }

+ 14 - 0
js/services.js

@@ -106,4 +106,18 @@ angular.module('kibana.services', [])
     timers = new Array();
   }
 
+})
+.service('keylistener', function($rootScope) {
+    var keys = [];
+    $(document).keydown(function (e) {
+      keys[e.which] = true;
+    });
+
+    $(document).keyup(function (e) {
+      delete keys[e.which];
+    });
+
+    this.keyActive = function(key) {
+      return keys[key] == true;
+    }
 });

+ 92 - 0
panels/map2/display/binning.js

@@ -0,0 +1,92 @@
+/**
+ * Hexagonal binning
+ * Rendered as normally projected svg paths, which mean they *do not*
+ * clip on spheres appropriately.  To fix this, we would need to translate
+ * the svg path into a geo-path
+ */
+function displayBinning(scope, dr, dimensions) {
+
+    var hexbin = d3.hexbin()
+        .size(dimensions)
+        .radius(scope.panel.display.binning.hexagonSize);
+
+
+    var binPoints = [],
+        binnedPoints = [],
+        binRange = 0;
+
+
+    if (scope.panel.display.binning.enabled) {
+        /**
+         * 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 = 50/max;
+
+            _.map(scope.data, function (k, v) {
+                var decoded = geohash.decode(v);
+                return _.map(_.range(0, k*scale), function(a,b) {
+                    binPoints.push(dr.projection([decoded.longitude, decoded.latitude]));
+                })
+            });
+
+        } else {
+            binPoints = dr.projectedPoints;
+        }
+
+        //bin and sort the points, so we can set the various ranges appropriately
+        binnedPoints = hexbin(binPoints).sort(function(a, b) { return b.length - a.length; });
+        binRange = binnedPoints[0].length;
+
+        //clean up some memory
+        binPoints = [];
+    } else {
+
+        //not enabled, so just set an empty array.  D3.exit will take care of the rest
+        binnedPoints = [];
+        binRange = 0;
+    }
+
+
+
+    var radius = d3.scale.sqrt()
+        .domain([0, binRange])
+        .range([0, scope.panel.display.binning.hexagonSize]);
+
+    var color = d3.scale.linear()
+        .domain([0,binRange])
+        .range(["white", "steelblue"])
+        .interpolate(d3.interpolateLab);
+
+
+    var hex = dr.g.selectAll(".hexagon")
+        .data(binnedPoints);
+
+    hex.enter().append("path")
+        .attr("d", function (d) {
+            if (scope.panel.display.binning.areaEncoding === false) {
+                return hexbin.hexagon();
+            } else {
+                return hexbin.hexagon(radius(d.length));
+            }
+        })
+        .attr("class", "hexagon")
+        .attr("transform", function (d) {
+            return "translate(" + d.x + "," + d.y + ")";
+        })
+        .style("fill", function (d) {
+            if (scope.panel.display.binning.colorEncoding === false) {
+                return color(binnedPoints[0].length / 2);
+            } else {
+                return color(d.length);
+            }
+        })
+        .attr("opacity", scope.panel.display.binning.hexagonAlpha);
+
+    hex.exit().remove();
+}

+ 28 - 0
panels/map2/display/bullseye.js

@@ -0,0 +1,28 @@
+/**
+ * Renders bullseyes as geo-json poly gon entities
+ * Allows for them to clip on spheres correctly
+ */
+function displayBullseye(scope, dr) {
+
+    var degrees = 180 / Math.PI
+    var circle = d3.geo.circle();
+    var data = [];
+
+    if (scope.panel.display.bullseye.enabled) {
+        data =  [
+          circle.origin(parseFloat(scope.panel.display.bullseye.coord.lat), parseFloat(scope.panel.display.bullseye.coord.lon)).angle(1000 / 6371 * degrees)()
+        ];
+    }
+
+    var arcs = dr.g.selectAll(".arc")
+        .data(data);
+
+    arcs.enter().append("path")
+
+        .attr("d", dr.path)
+        .attr("class", "arc");
+
+    arcs.exit().remove();
+
+
+}

+ 44 - 0
panels/map2/display/geopoints.js

@@ -0,0 +1,44 @@
+/**
+ * Renders geopoints as geo-json poly gon entities
+ * Allows for them to clip on spheres correctly
+ */
+function displayGeopoints(scope, dr) {
+
+    var points = [];
+    var circle = d3.geo.circle();
+    var degrees = 180 / Math.PI
+
+    if (scope.panel.display.geopoints.enabled) {
+        //points = dr.points;
+
+      points = _.map(dr.points, function(v) {
+        return {
+            type: "Point",
+            coordinates: [v[0], v[1]]
+        };
+      });
+
+    }
+
+
+    dr.geopoints = dr.g.selectAll("path.geopoint")
+      .data(points);
+
+
+
+    dr.geopoints.enter().append("path")
+      /*
+        .datum(function(d) {
+            return circle.origin([d[0], d[1]]).angle(scope.panel.display.geopoints.pointSize / 6371 * degrees)();
+        })
+        */
+      .attr("class", "geopoint")
+      .attr("d", dr.path);
+
+    dr.geopoints.exit().remove();
+
+
+
+
+
+}

+ 287 - 0
panels/map2/editor.html

@@ -0,0 +1,287 @@
+<style>
+    .tabDetails {
+        border-bottom: 1px solid #ddd;
+        padding-bottom:20px;
+    }
+    .tabDetails td {
+        padding-right: 10px;
+        padding-bottom:10px;
+    }
+</style>
+<div ng-controller="map2">
+    <div class="row-fluid" style="margin-bottom:20px">
+        <div class="span11">
+            The map panel is compatible with either Geopoints or two-letter country codes, depending on the graphing options.  Left click to drag/pan map, scroll wheel (or double click) to zoom.  Globes can be spun using ctrl-key + drag.
+        </div>
+    </div>
+
+
+    <div class="row-fluid">
+        <div class="span10">
+            <form class="form-horizontal">
+                <div class="control-group">
+                    <label class="control-label" for="panelfield">Primary Field</label>
+                    <div class="controls">
+                        <input type="text" id="panelfield" class="input"
+                               ng-model="panel.field"
+                               ng-change="get_data()" />
+                    </div>
+                </div>
+                <div class="control-group">
+                    <label class="control-label" for="panelsecondaryfield">Secondary Field</label>
+                    <div class="controls">
+                        <input type="text" id="panelsecondaryfield" class="input"
+                               ng-model="panel.secondaryfield"
+                               ng-change="get_data()"
+                               data-placement="right"
+                               placeholder="Optional"
+                               bs-tooltip="'Allows aggregating on Primary field, while counting stats on a secondary (e.g. Group By user_id, Sum(purchase_price)).'" />
+                    </div>
+                </div>
+                <div class="control-group">
+                    <label class="control-label" for="panelquery">Query</label>
+                    <div class="controls">
+                        <input type="text" id="panelquery" class="input" ng-model="panel.query">
+                    </div>
+                </div>
+            </form>
+        </div>
+    </div>
+    <div class="row-fluid">
+        <div class="span11">
+            <h4>Display Options</h4>
+        </div>
+        <!--
+            Rolling our own tab control here because the Angular-Strap Tab directive doesn't allow
+            updates to components inside, which is quite bizarre.  Or I just can't figure it out...
+        -->
+
+        <div class="span11">
+            <ul class="nav nav-tabs" ng-cloak="">
+                <li ng-repeat="tab in ['Geopoints', 'Binning', 'Choropleth', 'Bullseye', 'Data']" ng-class="{active:isActive(tab)}">
+                    <a ng-click="tabClick(tab)">{{tab}}</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+
+    <div class="row-fluid tabDetails" ng-show="isActive('geopoints')">
+        <div class="span8 offset1">
+            <table>
+                <tbody  >
+                <tr>
+                    <td>Geopoints</td>
+                    <td>
+                        <button type="button" class="btn" bs-button
+                            data-placement="right"
+                            bs-tooltip="'Compatible with Geopoint Type'"
+                            ng-change="$emit('render')"
+                            ng-class="{'btn-success': panel.display.geopoints.enabled}"
+                            ng-model="panel.display.geopoints.enabled">{{panel.display.geopoints.enabled|enabledText}}</button>
+                    </td>
+                </tr>
+                <tr>
+                    <td>Point size</td>
+                    <td>
+                        <input type="text" style="width:100px"
+                            ng-change="$emit('render')"
+                            data-placement="right"
+                            bs-tooltip="'Controls the size of the geopoints on the map.'"
+                            ng-model="panel.display.geopoints.pointSize"
+                            value="{{panel.display.geopoints.pointSize}}" />
+                    </td>
+                </tr>
+                <tr>
+                    <td>Point Transparency</td>
+                    <td>
+                        <input type="text" style="width:100px"
+                               ng-change="$emit('render')"
+                               data-placement="right"
+                               bs-tooltip="'Controls the transparency of geopoints. Valid numbers are between 0.0 and 1.0'"
+                               ng-model="panel.display.geopoints.pointAlpha"
+                               value="{{panel.display.geopoints.pointAlpha}}" />
+                    </td>
+                </tr>
+                <tr>
+                    <td>Autosizing</td>
+                    <td>
+                        <input type="checkbox"
+                               ng-change="$emit('render')"
+                               data-placement="right"
+                               ng-model="panel.display.geopoints.autosize"
+                               bs-tooltip="'Allows point sizes to scale as you zoom in and out of the map.'" />
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+
+    <div class="row-fluid tabDetails" ng-show="isActive('binning')">
+        <div class="span8 offset1">
+            <table>
+                <tbody  >
+                <tr>
+                    <td>Binning</td>
+                    <td>
+                        <button type="button" class="btn" bs-button
+                                data-placement="right"
+                                bs-tooltip="'Compatible with Geopoint Type'"
+                                ng-change="$emit('render')"
+                                ng-class="{'btn-success': panel.display.binning.enabled}"
+                                ng-model="panel.display.binning.enabled">{{panel.display.binning.enabled|enabledText}}</button>
+                    </td>
+                </tr>
+                <tr>
+                    <td>Hexagon size</td>
+                    <td>
+                        <input type="text" style="width:100px"
+                               ng-change="$emit('render')"
+                               data-placement="right"
+                               bs-tooltip="'Controls the size of the hexagonal binning'"
+                               ng-model="panel.display.binning.hexagonSize"
+                               value="{{panel.display.binning.hexagonSize}}" />
+                    </td>
+                </tr>
+                <tr>
+                    <td>Hexagon Transparency</td>
+                    <td>
+                        <input type="text" style="width:100px"
+                               ng-change="$emit('render')"
+                               data-placement="right"
+                               bs-tooltip="'Controls the transparency of hexagonal bins. Valid numbers are between 0.0 and 1.0'"
+                               ng-model="panel.display.binning.hexagonAlpha"
+                               value="{{panel.display.binning.hexagonAlpha}}" />
+                    </td>
+                </tr>
+                <tr>
+                    <td>
+                        <button type="button" class="btn" bs-button
+                                ng-change="$emit('render')"
+                                ng-class="{'btn-success': panel.display.binning.areaEncoding}"
+                                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>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+
+    <div class="row-fluid tabDetails" ng-show="isActive('choropleth')">
+        <div class="span8 offset1">
+            <table>
+                <tbody  >
+                <tr>
+                    <td>Choropleth</td>
+                    <td>
+                        <button type="button" class="btn" bs-button
+                                data-placement="right"
+                                bs-tooltip="'Choropleths color country regions according to your selected field. Compatible with Country-Coded fields'"
+                                ng-change="$emit('render')"
+                                ng-class="{'btn-success': panel.display.choropleth.enabled}"
+                                ng-model="panel.display.choropleth.enabled">{{panel.display.choropleth.enabled|enabledText}}</button>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+
+    <div class="row-fluid tabDetails" ng-show="isActive('bullseye')">
+        <div class="span8 offset1">
+            <table>
+                <tbody  >
+                <tr>
+                    <td>Bullseye</td>
+                    <td>
+                        <button type="button" class="btn" bs-button
+                                ng-change="$emit('render')"
+                                ng-class="{'btn-success': panel.display.bullseye.enabled}"
+                                ng-model="panel.display.bullseye.enabled">{{panel.display.bullseye.enabled|enabledText}}</button>
+                    </td>
+                </tr>
+                <tr>
+                    <td>Bullseye Coordinates</td>
+                    <td>
+                        <input type="text" style="width:100px"
+                               ng-change="$emit('render')"
+                               placeholder="Latitude"
+                               data-placement="right"
+                               bs-tooltip="'Latitude of Bullseye'"
+                               ng-model="panel.display.bullseye.coord.lat"
+                               value="{{panel.display.bullseye.coord.lat}}" />
+
+                        <input type="text" style="width:100px"
+                               placeholder="Longitude"
+                               ng-change="$emit('render')"
+                               data-placement="right"
+                               bs-tooltip="'Longitude of Bullseye'"
+                               ng-model="panel.display.bullseye.coord.lon"
+                               value="{{panel.display.bullseye.coord.lon}}" />
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+
+    <div class="row-fluid tabDetails" ng-show="isActive('data')">
+        <div class="span8 offset1">
+            <table>
+                <tbody  >
+                <tr>
+                    <td>Data Points</td>
+                    <td>
+                        <input type="text" style="width:100px"
+                               ng-change="get_data()"
+                               data-placement="right"
+                               bs-tooltip="'Controls the number of samples used in the map. Be careful with this value!'"
+                               ng-model="panel.display.data.samples"
+                               value="{{panel.display.data.samples}}" />
+                    </td>
+                </tr>
+                <tr>
+                    <td>Map Projection</td>
+                    <td>
+                        <select ng-model="panel.display.data.type" ng-options="option.id as option.text for option in panel.display.data.dropdown"></select>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+
+    <h5>Panel Spy</h5>
+
+    <div class="row-fluid">
+        <div class="span2">
+            <label class="small">Spyable</label> <input type="checkbox" ng-model=
+                "panel.spyable" ng-checked="panel.spyable">
+        </div>
+
+        <div class="span9 small">
+            The panel spy shows 'behind the scenes' information about a panel. It can be
+            accessed by clicking the in the top right of the panel.
+        </div>
+    </div>
+</div>

+ 1 - 0
panels/map2/lib/d3.hexbin.v0.min.js

@@ -0,0 +1 @@
+(function(){d3.hexbin=function(){function u(n){var r={};return n.forEach(function(n,t){var a=s.call(u,n,t)/o,e=Math.round(a),h=f.call(u,n,t)/i-(1&e?.5:0),c=Math.round(h),l=a-e;if(3*Math.abs(l)>1){var g=h-c,v=c+(c>h?-1:1)/2,M=e+(e>a?-1:1),m=h-v,d=a-M;g*g+l*l>m*m+d*d&&(c=v+(1&e?1:-1)/2,e=M)}var x=c+"-"+e,j=r[x];j?j.push(n):(j=r[x]=[n],j.x=(c+(1&e?.5:0))*i,j.y=e*o)}),d3.values(r)}function a(r){var t=0,u=0;return n.map(function(n){var a=Math.sin(n)*r,e=-Math.cos(n)*r,i=a-t,o=e-u;return t=a,u=e,[i,o]})}var e,i,o,h=1,c=1,f=r,s=t;return u.x=function(n){return arguments.length?(f=n,u):f},u.y=function(n){return arguments.length?(s=n,u):s},u.hexagon=function(n){return 1>arguments.length&&(n=e),"m"+a(n).join("l")+"z"},u.mesh=function(){for(var n=[],r=a(e).slice(0,4).join("l"),t=0,u=!1;c+e>t;t+=o,u=!u)for(var f=u?i/2:0;h>f;f+=i)n.push("M",f,",",t,"m",r);return n.join("")},u.size=function(n){return arguments.length?(h=+n[0],c=+n[1],u):[h,c]},u.radius=function(n){return arguments.length?(e=+n,i=2*e*Math.sin(Math.PI/3),o=1.5*e,u):e},u.radius(1)};var n=d3.range(0,2*Math.PI,Math.PI/3),r=function(n){return n[0]},t=function(n){return n[1]}})();

+ 137 - 0
panels/map2/lib/node-geohash.js

@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2011, Sun Ning.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+var BASE32_CODES = "0123456789bcdefghjkmnpqrstuvwxyz";
+var BASE32_CODES_DICT = {};
+for(var i=0; i<BASE32_CODES.length; i++) {
+    BASE32_CODES_DICT[BASE32_CODES.charAt(i)]=i;
+}
+
+var encode = function(latitude, longitude, numberOfChars){
+    numberOfChars = numberOfChars || 9;
+    var chars = [], bits = 0;
+    var hash_value = 0;
+
+    var maxlat = 90, minlat = -90;
+    var maxlon = 180, minlon = -180;
+
+    var mid;
+    var islon = true;
+    while(chars.length < numberOfChars) {
+        if (islon){
+            mid = (maxlon+minlon)/2;
+            if(longitude > mid){
+                hash_value = (hash_value << 1) + 1;
+                minlon=mid;
+            } else {
+                hash_value = (hash_value << 1) + 0;
+                maxlon=mid;
+            }
+        } else {
+            mid = (maxlat+minlat)/2;
+            if(latitude > mid ){
+                hash_value = (hash_value << 1) + 1;
+                minlat = mid;
+            } else {
+                hash_value = (hash_value << 1) + 0;
+                maxlat = mid;
+            }
+        }
+        islon = !islon;
+
+        bits++;
+        if (bits == 5) {
+            var code = BASE32_CODES[hash_value];
+            chars.push(code);
+            bits = 0;
+            hash_value = 0;
+        }
+    }
+    return chars.join('')
+};
+
+var decode_bbox = function(hash_string){
+    var islon = true;
+    var maxlat = 90, minlat = -90;
+    var maxlon = 180, minlon = -180;
+
+    var hash_value = 0;
+    for(var i=0,l=hash_string.length; i<l; i++) {
+        var code = hash_string[i].toLowerCase();
+        hash_value = BASE32_CODES_DICT[code];
+
+        for (var bits=4; bits>=0; bits--) {
+            var bit = (hash_value >> bits) & 1;
+            if (islon){
+                var mid = (maxlon+minlon)/2;
+                if(bit == 1){
+                    minlon = mid;
+                } else {
+                    maxlon = mid;
+                }
+            } else {
+                var mid = (maxlat+minlat)/2;
+                if(bit == 1){
+                    minlat = mid;
+                } else {
+                    maxlat = mid;
+                }
+            }
+            islon = !islon;
+        }
+    }
+    return [minlat, minlon, maxlat, maxlon];
+}
+
+var decode = function(hash_string){
+    var bbox = decode_bbox(hash_string);
+    var lat = (bbox[0]+bbox[2])/2;
+    var lon = (bbox[1]+bbox[3])/2;
+    var laterr = bbox[2]-lat;
+    var lonerr = bbox[3]-lon;
+    return {latitude:lat, longitude:lon,
+        error:{latitude:laterr, longitude:lonerr}};
+};
+
+/**
+ * direction [lat, lon], i.e.
+ * [1,0] - north
+ * [1,1] - northeast
+ * ...
+ */
+var neighbor = function(hashstring, direction) {
+    var lonlat = decode(hashstring);
+    var neighbor_lat = lonlat.latitude
+        + direction[0] * lonlat.error.latitude * 2;
+    var neighbor_lon = lonlat.longitude
+        + direction[1] * lonlat.error.longitude * 2;
+    return encode(neighbor_lat, neighbor_lon, hashstring.length);
+}
+
+var geohash = {
+    'encode': encode,
+    'decode': decode,
+    'decode_bbox': decode_bbox,
+    'neighbor': neighbor,
+}
+module.exports = geohash;

+ 1 - 0
panels/map2/lib/queue.v1.min.js

@@ -0,0 +1 @@
+(function(){function n(n){function t(){for(;f=a<c.length&&n>p;){var u=a++,t=c[u],r=l.call(t,1);r.push(e(u)),++p,t[0].apply(null,r)}}function e(n){return function(u,l){--p,null==d&&(null!=u?(d=u,a=s=0/0,r()):(c[n]=l,--s?f||t():r()))}}function r(){null!=d?v(d):i?v(d,c):v.apply(null,[d].concat(c))}var o,f,i,c=[],a=0,p=0,s=0,d=null,v=u;return n||(n=1/0),o={defer:function(){return d||(c.push(arguments),++s,t()),o},await:function(n){return v=n,i=!1,s||r(),o},awaitAll:function(n){return v=n,i=!0,s||r(),o}}}function u(){}"undefined"==typeof module?self.queue=n:module.exports=n,n.version="1.0.4";var l=[].slice})();

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
panels/map2/lib/topojson.v1.min.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
panels/map2/lib/world-110m.json


+ 251 - 0
panels/map2/lib/world-country-names.tsv

@@ -0,0 +1,251 @@
+id	short	name
+4	AF	Afghanistan
+8	AL	Albania
+10	AQ	Antarctica
+12	DZ	Algeria
+16	AS	American Samoa
+20	AD	Andorra
+24	AO	Angola
+28	AG	Antigua and Barbuda
+31	AZ	Azerbaijan
+32	AR	Argentina
+36	AU	Australia
+40	AT	Austria
+44	BS	Bahamas
+48	BH	Bahrain
+50	BD	Bangladesh
+51	AM	Armenia
+52	BB	Barbados
+56	BE	Belgium
+60	BM	Bermuda
+64	BT	Bhutan
+68	BO	Bolivia, Plurinational State of
+70	BA	Bosnia and Herzegovina
+72	BW	Botswana
+74	BV	Bouvet Island
+76	BR	Brazil
+84	BZ	Belize
+86	IO	British Indian Ocean Territory
+90	SB	Solomon Islands
+92	VG	Virgin Islands, British
+96	BN	Brunei Darussalam
+100	BG	Bulgaria
+104	MM	Myanmar
+108	BI	Burundi
+112	BY	Belarus
+116	KH	Cambodia
+120	CM	Cameroon
+124	CA	Canada
+132	CV	Cape Verde
+136	KY	Cayman Islands
+140	CF	Central African Republic
+144	LK	Sri Lanka
+148	TD	Chad
+152	CL	Chile
+156	CN	China
+158	TW	Taiwan, Province of China
+162	CX	Christmas Island
+166	CC	Cocos (Keeling) Islands
+170	CO	Colombia
+174	KM	Comoros
+175	YT	Mayotte
+178	CG	Congo
+180	CD	Congo, the Democratic Republic of the
+184	CK	Cook Islands
+188	CR	Costa Rica
+191	HR	Croatia
+192	CU	Cuba
+196	CY	Cyprus
+203	CZ	Czech Republic
+204	BJ	Benin
+208	DK	Denmark
+212	DM	Dominica
+214	DO	Dominican Republic
+218	EC	Ecuador
+222	SV	El Salvador
+226	GQ	Equatorial Guinea
+231	ET	Ethiopia
+232	ER	Eritrea
+233	EE	Estonia
+234	FO	Faroe Islands
+238	FK	Falkland Islands (Malvinas)
+239	GS	South Georgia and the South Sandwich Islands
+242	FJ	Fiji
+246	FI	Finland
+248	AX	Åland Islands
+250	FR	France
+254	GF	French Guiana
+258	PF	French Polynesia
+260	TF	French Southern Territories
+262	DJ	Djibouti
+266	GA	Gabon
+268	GE	Georgia
+270	GM	Gambia
+275		Palestinian Territory, Occupied
+276	DE	Germany
+288	GH	Ghana
+292	GI	Gibraltar
+296	KI	Kiribati
+300	GR	Greece
+304	GL	Greenland
+308	GD	Grenada
+312	GP	Guadeloupe
+316	GU	Guam
+320	GT	Guatemala
+324	GN	Guinea
+328	GY	Guyana
+332	HT	Haiti
+334	HM	Heard Island and McDonald Islands
+336	VA	Holy See (Vatican City State)
+340	HN	Honduras
+344	HK	Hong Kong
+348	HU	Hungary
+352	IS	Iceland
+356	IN	India
+360	ID	Indonesia
+364	IR	Iran, Islamic Republic of
+368	IQ	Iraq
+372	IE	Ireland
+376	IL	Israel
+380	IT	Italy
+384		Côte d'Ivoire
+388	JM	Jamaica
+392	JP	Japan
+398	KZ	Kazakhstan
+400	JO	Jordan
+404	KE	Kenya
+408	KP	Korea, Democratic People's Republic of
+410	KR	Korea, Republic of
+414	KW	Kuwait
+417	KG	Kyrgyzstan
+418	LA	Lao People's Democratic Republic
+422	LB	Lebanon
+426	LS	Lesotho
+428	LV	Latvia
+430	LR	Liberia
+434	LY	Libya
+438	LI	Liechtenstein
+440	LT	Lithuania
+442	LU	Luxembourg
+446	MO	Macao
+450	MG	Madagascar
+454	MW	Malawi
+458	MY	Malaysia
+462	MV	Maldives
+466	ML	Mali
+470	MT	Malta
+474	MQ	Martinique
+478	MR	Mauritania
+480	MU	Mauritius
+484	MX	Mexico
+492	MC	Monaco
+496	MN	Mongolia
+498	MD	Moldova, Republic of
+499	ME	Montenegro
+500	MS	Montserrat
+504	MA	Morocco
+508	MZ	Mozambique
+512	OM	Oman
+516	NA	Namibia
+520	NR	Nauru
+524	NP	Nepal
+528	NL	Netherlands
+531		Curaçao
+533	AW	Aruba
+534	SX	Sint Maarten (Dutch part)
+535	BQ	Bonaire, Sint Eustatius and Saba
+540	NC	New Caledonia
+548	VU	Vanuatu
+554	NZ	New Zealand
+558	NI	Nicaragua
+562	NE	Niger
+566	NG	Nigeria
+570	NU	Niue
+574	NF	Norfolk Island
+578	NO	Norway
+580	MP	Northern Mariana Islands
+581	UM	United States Minor Outlying Islands
+583	FM	Micronesia, Federated States of
+584	MH	Marshall Islands
+585	PW	Palau
+586	PK	Pakistan
+591	PA	Panama
+598	PG	Papua New Guinea
+600	PY	Paraguay
+604	PE	Peru
+608	PH	Philippines
+612	PN	Pitcairn
+616	PL	Poland
+620	PT	Portugal
+624	GW	Guinea-Bissau
+626	TL	Timor-Leste
+630	PR	Puerto Rico
+634	QA	Qatar
+638		Réunion
+642	RO	Romania
+643	RU	Russian Federation
+646	RW	Rwanda
+652		Saint Barthélemy
+654	SH	Saint Helena, Ascension and Tristan da Cunha
+659	KN	Saint Kitts and Nevis
+660	AI	Anguilla
+662	LC	Saint Lucia
+663	MF	Saint Martin (French part)
+666	PM	Saint Pierre and Miquelon
+670	VC	Saint Vincent and the Grenadines
+674	SM	San Marino
+678	ST	Sao Tome and Principe
+682	SA	Saudi Arabia
+686	SN	Senegal
+688	RS	Serbia
+690	SC	Seychelles
+694	SL	Sierra Leone
+702	SG	Singapore
+703	SK	Slovakia
+704	VN	Viet Nam
+705	SI	Slovenia
+706	SO	Somalia
+710	ZA	South Africa
+716	ZW	Zimbabwe
+724	ES	Spain
+728	SS	South Sudan
+729	SD	Sudan
+732	EH	Western Sahara
+740	SR	Suriname
+744	SJ	Svalbard and Jan Mayen
+748	SZ	Swaziland
+752	SE	Sweden
+756	CH	Switzerland
+760	SY	Syrian Arab Republic
+762	TJ	Tajikistan
+764	TH	Thailand
+768	TG	Togo
+772	TK	Tokelau
+776	TO	Tonga
+780	TT	Trinidad and Tobago
+784	AE	United Arab Emirates
+788	TN	Tunisia
+792	TR	Turkey
+795	TM	Turkmenistan
+796	TC	Turks and Caicos Islands
+798	TV	Tuvalu
+800	UG	Uganda
+804	UA	Ukraine
+807	MK	Macedonia, the former Yugoslav Republic of
+818	EG	Egypt
+826	GB	United Kingdom
+831	GG	Guernsey
+832	JE	Jersey
+833	IM	Isle of Man
+834	TZ	Tanzania, United Republic of
+840	US	United States
+850	VI	Virgin Islands, U.S.
+854	BF	Burkina Faso
+858	UY	Uruguay
+860	UZ	Uzbekistan
+862	VE	Venezuela, Bolivarian Republic of
+876	WF	Wallis and Futuna
+882	WS	Samoa
+887	YE	Yemen
+894	ZM	Zambia
+		

+ 58 - 0
panels/map2/module.html

@@ -0,0 +1,58 @@
+<kibana-panel ng-controller='map2' ng-init="init()">
+    <style>
+        .overlay {
+            fill: none;
+            pointer-events: all;
+        }
+
+        .land {
+            fill: #D1D1D1;
+            stroke: #595959;
+            stroke-linejoin: round;
+            stroke-linecap: round;
+            stroke-width: .1px;
+        }
+
+        .boundary {
+            fill: none;
+            stroke: #000;
+            stroke-linejoin: round;
+            stroke-linecap: round;
+        }
+
+        .hexagon {
+            fill: none;
+            stroke: #000;
+            stroke-width: .1px;
+        }
+
+        .q1 { fill:rgb(247,251,255); }
+        .q2 { fill:rgb(222,235,247); }
+        .q3 { fill:rgb(198,219,239); }
+        .q4 { fill:rgb(158,202,225); }
+        .q5 { fill:rgb(107,174,214); }
+        .q6 { fill:rgb(66,146,198); }
+        .q7 { fill:rgb(33,113,181); }
+        .q8 { fill:rgb(8,81,156); }
+        .q9 { fill:rgb(8,48,107); }
+
+        .arc {
+            stroke: #f00;
+            stroke-width: .5px;
+            fill: none;
+        }
+
+        .geopoint {
+            stroke: #000;
+            stroke-width: .5px;
+            fill: #000;
+        }
+
+        .dropdown-menu{position:absolute;top:auto;left:auto;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#ffffff;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;}
+    </style>
+  <span ng-show="panel.spyable" style="position:absolute;right:0px;top:0px" class='panelextra pointer'>
+    <i bs-modal="'partials/modal.html'" class="icon-eye-open"></i>
+  </span>
+
+  <div map2 params="{{panel}}" style="height:{{panel.height || row.height}}"></div>
+</kibana-panel>

+ 451 - 0
panels/map2/module.js

@@ -0,0 +1,451 @@
+angular.module('kibana.map2', [])
+  .controller('map2', function ($scope, eventBus, keylistener) {
+
+    // Set and populate defaults
+    var _d = {
+      query: "*",
+      map: "world",
+      colors: ['#C8EEFF', '#0071A4'],
+      size: 100,
+      exclude: [],
+      spyable: true,
+      group: "default",
+      index_limit: 0,
+      display: {
+        translate:[0, 0],
+        scale:-1,
+        data: {
+          samples: 1000,
+          type: "mercator",
+          dropdown:[
+            {
+              "text": "Mercator (Flat)",
+              id: "mercator"
+            },
+            {
+              text: "Orthographic (Sphere)",
+              id: "orthographic"
+            }
+          ]
+        },
+        geopoints: {
+          enabled: false,
+          enabledText: "Enabled",
+          pointSize: 0.3,
+          pointAlpha: 0.6
+        },
+        binning: {
+          enabled: false,
+          hexagonSize: 2,
+          hexagonAlpha: 1.0,
+          areaEncoding: true,
+          areaEncodingField: "primary",
+          colorEncoding: true,
+          colorEncodingField: "primary"
+        },
+        choropleth: {
+          enabled: false
+        },
+        bullseye: {
+          enabled: false,
+          coord: {
+            lat: 0,
+            lon: 0
+          }
+        }
+      },
+      activeDisplayTab:"Geopoints"
+    };
+
+    _.defaults($scope.panel, _d)
+
+    $scope.init = function () {
+      eventBus.register($scope, 'time', function (event, time) {
+        set_time(time)
+      });
+      eventBus.register($scope, 'query', function (event, query) {
+        $scope.panel.query = _.isArray(query) ? query[0] : query;
+        $scope.get_data();
+      });
+      // Now that we're all setup, request the time from our group
+      eventBus.broadcast($scope.$id, $scope.panel.group, 'get_time');
+
+      $scope.keylistener = keylistener;
+
+    };
+
+    $scope.get_data = function () {
+
+      // Make sure we have everything for the request to complete
+      if (_.isUndefined($scope.panel.index) || _.isUndefined($scope.time))
+        return
+
+      $scope.panel.loading = true;
+      var request = $scope.ejs.Request().indices($scope.panel.index);
+
+
+      var metric = 'count';
+
+      //Use a regular term facet if there is no secondary field
+      if (typeof $scope.panel.secondaryfield === "undefined") {
+        var facet = $scope.ejs.TermsFacet('map')
+          .field($scope.panel.field)
+          .size($scope.panel.display.data.samples)
+          .exclude($scope.panel.exclude)
+          .facetFilter(ejs.QueryFilter(
+            ejs.FilteredQuery(
+              ejs.QueryStringQuery($scope.panel.query || '*'),
+              ejs.RangeFilter($scope.time.field)
+                .from($scope.time.from)
+                .to($scope.time.to))));
+
+        metric = 'count';
+      } 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))));
+
+        metric = 'total';
+      }
+
+
+      // Then the insert into facet and make the request
+      var request = request.facet(facet).size(0);
+
+      $scope.populate_modal(request);
+
+      var results = request.doSearch();
+
+      // Populate scope when we have results
+      results.then(function (results) {
+        $scope.panel.loading = false;
+        $scope.hits = results.hits.total;
+        $scope.data = {};
+
+        _.each(results.facets.map.terms, function (v) {
+
+          if (!_.isNumber(v.term)) {
+            $scope.data[v.term.toUpperCase()] = v[metric];
+          } else {
+            $scope.data[v.term] = v[metric];
+          }
+        });
+
+        $scope.$emit('render')
+      });
+    };
+
+    // I really don't like this function, too much dom manip. Break out into directive?
+    $scope.populate_modal = function (request) {
+      $scope.modal = {
+        title: "Inspector",
+        body: "<h5>Last Elasticsearch Query</h5><pre>" + 'curl -XGET ' + config.elasticsearch + '/' + $scope.panel.index + "/_search?pretty -d'\n" + angular.toJson(JSON.parse(request.toString()), true) + "'</pre>"
+      }
+    };
+
+    function set_time(time) {
+      $scope.time = time;
+      $scope.panel.index = _.isUndefined(time.index) ? $scope.panel.index : time.index
+      $scope.get_data();
+    }
+
+    $scope.build_search = function (field, value) {
+      $scope.panel.query = add_to_query($scope.panel.query, field, value, false)
+      $scope.get_data();
+      eventBus.broadcast($scope.$id, $scope.panel.group, 'query', $scope.panel.query);
+    };
+
+    $scope.isActive = function(tab) {
+      return (tab.toLowerCase() === $scope.panel.activeDisplayTab.toLowerCase());
+    }
+
+    $scope.tabClick = function(tab) {
+      $scope.panel.activeDisplayTab = tab;
+    }
+
+  })
+  .filter('enabledText', function() {
+    return function (value) {
+      if (value === true) {
+        return "Enabled";
+      } else {
+        return "Disabled";
+      }
+    }
+  })
+  .directive('map2', function () {
+    return {
+      restrict: 'A',
+      link: function (scope, elem, attrs) {
+
+        //directive level variables related to d3
+        var dr = {};
+
+        scope.initializing = false;
+
+
+        dr.worldData = null;
+        dr.worldNames = null;
+
+        /**
+         * Initialize the panels if new, or render existing panels
+         */
+        scope.init_or_render = function() {
+          if (typeof dr.svg === 'undefined') {
+            console.log("init");
+
+            //prevent duplicate initialization steps, if render is called again
+            //before the svg is setup
+            if (!scope.initializing) {
+              init_panel();
+            }
+          } else {
+            console.log("render");
+            render_panel();
+          }
+        };
+
+
+        /**
+         * Receive render events
+         */
+        scope.$on('render', function () {
+          scope.init_or_render();
+        });
+
+        /**
+         * On window resize, re-render the panel
+         */
+        angular.element(window).bind('resize', function () {
+          scope.init_or_render();
+        });
+
+
+        /**
+         * Load the various panel-specific scripts, map data, then initialize
+         * the svg and set appropriate D3 settings
+         */
+        function init_panel() {
+
+          scope.initializing = true;
+          // Using LABjs, wait until all scripts are loaded before rendering panel
+          var scripts = $LAB.script("common/lib/d3.v3.min.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/lib/topojson.v1.min.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/lib/node-geohash.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/lib/d3.hexbin.v0.min.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/lib/queue.v1.min.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/display/binning.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/display/geopoints.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/display/bullseye.js?rand="+Math.floor(Math.random()*10000));
+
+          // Populate element. Note that jvectormap appends, does not replace.
+          scripts.wait(function () {
+
+            queue()
+              .defer(d3.json, "panels/map2/lib/world-110m.json")
+              .defer(d3.tsv, "panels/map2/lib/world-country-names.tsv")
+              .await(function(error, world, names) {
+                dr.worldData = world;
+                dr.worldNames = names;
+
+                //Better way to get these values?  Seems kludgy to use jQuery on the div...
+                var width = $(elem[0]).width(),
+                  height = $(elem[0]).height();
+
+                //scale to whichever dimension is smaller, helps to ensure the whole map is displayed
+                dr.scale = (width > height) ? (height/5) : (width/5);
+
+                dr.zoom = d3.behavior.zoom()
+                  .scaleExtent([1, 20])
+                  .on("zoom", translate_map);
+
+                //used by choropleth
+                //@todo change domain so that it reflects the domain of the data
+                dr.quantize = d3.scale.quantize()
+                  .domain([0, 1000])
+                  .range(d3.range(9).map(function(i) { return "q" + (i+1); }));
+
+                //Extract name and two-letter codes for our countries
+                dr.countries = topojson.feature(dr.worldData, dr.worldData.objects.countries).features;
+
+                dr.countries = dr.countries.filter(function(d) {
+                  return dr.worldNames.some(function(n) {
+                    if (d.id == n.id) {
+                      d.name = n.name;
+                      return d.short = n.short;
+                    }
+                  });
+                }).sort(function(a, b) {
+                    return a.name.localeCompare(b.name);
+                  });
+
+                //create the new svg
+                dr.svg = d3.select(elem[0]).append("svg")
+                  .attr("width", "100%")
+                  .attr("height", "100%")
+                  .attr("viewBox", "0 0 " + width + " " + height)
+                  .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
+                  .call(dr.zoom);
+                dr.g = dr.svg.append("g");
+
+                scope.initializing = false;
+                render_panel();
+              });
+          });
+        }
+
+
+        /**
+         * Render updates to the SVG. Typically happens when the data changes (time, query)
+         * or when new options are selected
+         */
+        function render_panel() {
+
+          var width = $(elem[0]).width(),
+            height = $(elem[0]).height();
+
+          //Projection is dependant on the map-type
+          if (scope.panel.display.data.type === 'mercator') {
+            dr.projection = d3.geo.mercator()
+              .translate([width/2, height/2])
+              .scale(dr.scale);
+
+          } else if (scope.panel.display.data.type === 'orthographic') {
+            dr.projection = d3.geo.orthographic()
+              .translate([width/2, height/2])
+              .scale(100)
+              .clipAngle(90);
+
+            //recenters the sphere more towards the US...not really necessary
+            dr.projection.rotate([100 / 2, 20 / 2, dr.projection.rotate()[2]]);
+
+          }
+
+          dr.path = d3.geo.path()
+            .projection(dr.projection).pointRadius(0.2);
+
+          console.log(scope.data);
+
+          //Geocoded points are decoded into lonlat
+          dr.points = _.map(scope.data, function (k, v) {
+            //console.log(k,v);
+            var decoded = geohash.decode(v);
+            return [decoded.longitude, decoded.latitude];
+          });
+
+          //And also projected projected to x/y.  Both sets of points are used
+          //by different functions
+          dr.projectedPoints = _.map(dr.points, function (coords) {
+            return dr.projection(coords);
+          });
+
+          dr.svg.select(".overlay").remove();
+
+          dr.svg.append("rect")
+            .attr("class", "overlay")
+            .attr("width", width)
+            .attr("height", height)
+            .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
+
+
+          //Draw the countries, if this is a choropleth, draw with fancy colors
+          var countryPath = dr.g.selectAll(".land")
+            .data(dr.countries);
+
+          countryPath.enter().append("path")
+            .attr("class", function(d) {
+              if (scope.panel.display.choropleth.enabled) {
+                return 'land ' + dr.quantize(scope.data[d.short]);
+              } else {
+                return 'land';
+              }
+            })
+            .attr("d", dr.path);
+
+          countryPath.exit().remove();
+
+          //If this is a sphere, set up drag and keypress listeners
+          if (scope.panel.display.data.type === 'orthographic') {
+            dr.svg.style("cursor", "move")
+              .call(d3.behavior.drag()
+                .origin(function() { var rotate = dr.projection.rotate(); return {x: 2 * rotate[0], y: -2 * rotate[1]}; })
+                .on("drag", function() {
+                  if (scope.keylistener.keyActive(17)) {
+                    dr.projection.rotate([d3.event.x / 2, -d3.event.y / 2, dr.projection.rotate()[2]]);
+
+                    //dr.svg.selectAll("path").attr("d", dr.path);
+                    dr.g.selectAll("path").attr("d", dr.path);
+
+                  }
+                   }));
+
+
+          }
+
+          //Special fix for when the user changes from mercator -> orthographic
+          //The globe won't redraw automatically, we need to force it
+          if (scope.panel.display.data.type === 'orthographic') {
+            dr.svg.selectAll("path").attr("d", dr.path);
+          }
+
+
+          /**
+           * Display option rendering
+           * Order is important to render order here!
+           */
+
+          //@todo fix this
+          var dimensions = [width, height];
+          displayBinning(scope, dr, dimensions);
+          displayGeopoints(scope, dr);
+          displayBullseye(scope, dr);
+
+
+
+
+          //If the panel scale is not default (e.g. the user has moved the maps around)
+          //set the scale and position to the last saved config
+          if (scope.panel.display.scale != -1) {
+            dr.zoom.scale(scope.panel.display.scale).translate(scope.panel.display.translate);
+            dr.g.style("stroke-width", 1 / scope.panel.display.scale).attr("transform", "translate(" + scope.panel.display.translate + ") scale(" + scope.panel.display.scale + ")");
+
+          }
+
+        }
+
+
+        /**
+         * On D3 zoom events, pan/zoom the map
+         * Only applies if the ctrl-key is not pressed, so it doesn't clobber
+         * sphere dragging
+         */
+        function translate_map() {
+
+          var width = $(elem[0]).width(),
+            height = $(elem[0]).height();
+
+          if (! scope.keylistener.keyActive(17)) {
+            var t = d3.event.translate,
+              s = d3.event.scale;
+            t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0]));
+            t[1] = Math.min(height / 2 * (s - 1) + 230 * s, Math.max(height / 2 * (1 - s) - 230 * s, t[1]));
+            dr.zoom.translate(t);
+
+            scope.panel.display.translate = t;
+            scope.panel.display.scale = s;
+            dr.g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
+          }
+        }
+      }
+    };
+  });

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů