Commit d3debd1f by Zachary Tong

Big refactor, moving initialization code to own function, so that multiple…

Big refactor, moving initialization code to own function, so that multiple graphs play nice together
parent b26cf205
...@@ -28,10 +28,7 @@ function displayBinning(scope, dimensions, projection, path) { ...@@ -28,10 +28,7 @@ function displayBinning(scope, dimensions, projection, path) {
} else { } else {
binPoints = _.map(scope.data, function (k, v) { binPoints = scope.projectedPoints;
var decoded = geohash.decode(v);
return projection([decoded.longitude, decoded.latitude]);
});
} }
//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
......
...@@ -13,16 +13,30 @@ function displayGeopoints(scope, path) { ...@@ -13,16 +13,30 @@ function displayGeopoints(scope, path) {
*/ */
var points = []
if (scope.panel.display.bullseye.enabled) {
points = scope.points;
}
var circle = d3.geo.circle(); var circle = d3.geo.circle();
var degrees = 180 / Math.PI var degrees = 180 / Math.PI
scope.g.selectAll("circles.points") var geopoints = scope.g.selectAll("geopoints")
.data(points) .data(points);
.enter().append("path")
geopoints.enter().append("path")
.datum(function(d) { .datum(function(d) {
return circle.origin([d[0], d[1]]).angle(5 / 6371 * degrees)(); return circle.origin([d[0], d[1]]).angle(scope.panel.display.geopoints.pointSize / 6371 * degrees)();
}) })
.attr("d", path) .attr("d", path)
.attr("class", "geopoint"); .attr("class", "geopoint");
geopoints.exit().remove();
} }
\ No newline at end of file
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
<div class="span11"> <div class="span11">
<ul class="nav nav-tabs" ng-cloak=""> <ul class="nav nav-tabs" ng-cloak="">
<li ng-repeat="tab in panel.displayTabs" ng-class="{active:isActive(tab)}"> <li ng-repeat="tab in ['Geopoints', 'Binning', 'Choropleth', 'Bullseye', 'Data']" ng-class="{active:isActive(tab)}">
<a ng-click="tabClick(tab)">{{tab}}</a> <a ng-click="tabClick(tab)">{{tab}}</a>
</li> </li>
</ul> </ul>
...@@ -87,7 +87,7 @@ ...@@ -87,7 +87,7 @@
<input type="text" style="width:100px" <input type="text" style="width:100px"
ng-change="$emit('render')" ng-change="$emit('render')"
data-placement="right" data-placement="right"
bs-tooltip="'Controls the size of the geopoints on the map'" bs-tooltip="'Controls the size of the geopoints on the map. Units in kilometers (km)'"
ng-model="panel.display.geopoints.pointSize" ng-model="panel.display.geopoints.pointSize"
value="{{panel.display.geopoints.pointSize}}" /> value="{{panel.display.geopoints.pointSize}}" />
</td> </td>
...@@ -216,7 +216,7 @@ ...@@ -216,7 +216,7 @@
<button type="button" class="btn" bs-button <button type="button" class="btn" bs-button
ng-change="$emit('render')" ng-change="$emit('render')"
ng-class="{'btn-success': panel.display.bullseye.enabled}" ng-class="{'btn-success': panel.display.bullseye.enabled}"
ng-model="panel.display.bullseye.enabled">{{panel.display.choropleth.enabled|enabledText}}</button> ng-model="panel.display.bullseye.enabled">{{panel.display.bullseye.enabled|enabledText}}</button>
</td> </td>
</tr> </tr>
<tr> <tr>
...@@ -259,6 +259,12 @@ ...@@ -259,6 +259,12 @@
value="{{panel.display.data.samples}}" /> value="{{panel.display.data.samples}}" />
</td> </td>
</tr> </tr>
<tr>
<td>Map Projection</td>
<td>
<select ng-model="panel.display.data.type" ng-options="option.id as option.text for option in options.data.dropdown"></select>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
......
...@@ -7,11 +7,15 @@ ...@@ -7,11 +7,15 @@
.land { .land {
fill: #D1D1D1; fill: #D1D1D1;
stroke: #595959;
stroke-linejoin: round;
stroke-linecap: round;
stroke-width: .1px;
} }
.boundary { .boundary {
fill: none; fill: none;
stroke: #fff; stroke: #000;
stroke-linejoin: round; stroke-linejoin: round;
stroke-linecap: round; stroke-linecap: round;
} }
...@@ -43,9 +47,12 @@ ...@@ -43,9 +47,12 @@
stroke-width: .5px; stroke-width: .5px;
fill: #000; 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> </style>
<span ng-show="panel.spyable" style="position:absolute;right:0px;top:0px" class='panelextra pointer'> <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> <i bs-modal="'partials/modal.html'" class="icon-eye-open"></i>
</span> </span>
<div map2 params="{{panel}}" style="height:{{panel.height || row.height}}"></div> <div map2 params="{{panel}}" style="height:{{panel.height || row.height}}"></div>
</kibana-panel> </kibana-panel>
\ No newline at end of file
...@@ -15,16 +15,17 @@ angular.module('kibana.map2', []) ...@@ -15,16 +15,17 @@ angular.module('kibana.map2', [])
translate:[0, 0], translate:[0, 0],
scale:-1, scale:-1,
data: { data: {
samples: 1000 samples: 1000,
type: "mercator"
}, },
geopoints: { geopoints: {
enabled: true, enabled: false,
enabledText: "Enabled", enabledText: "Enabled",
pointSize: 0.3, pointSize: 0.3,
pointAlpha: 0.6 pointAlpha: 0.6
}, },
binning: { binning: {
enabled: true, enabled: false,
hexagonSize: 2, hexagonSize: 2,
hexagonAlpha: 1.0, hexagonAlpha: 1.0,
areaEncoding: true, areaEncoding: true,
...@@ -41,12 +42,8 @@ angular.module('kibana.map2', []) ...@@ -41,12 +42,8 @@ angular.module('kibana.map2', [])
lat: 0, lat: 0,
lon: 0 lon: 0
} }
},
map: {
type: "mercator"
} }
}, },
displayTabs: ["Geopoints", "Binning", "Choropleth", "Bullseye", "Data"],
activeDisplayTab:"Geopoints" activeDisplayTab:"Geopoints"
}; };
...@@ -188,95 +185,200 @@ angular.module('kibana.map2', []) ...@@ -188,95 +185,200 @@ angular.module('kibana.map2', [])
.directive('map2', function () { .directive('map2', function () {
return { return {
restrict: 'A', restrict: 'A',
template: '<div class="loading"><center><img src="common/img/load_big.gif" style="display:none"></center></div><div id="{{uuid}}"></div>',
link: function (scope, elem, attrs) { link: function (scope, elem, attrs) {
elem.html('<center><img src="common/img/load_big.gif"></center>')
//elem.html('')
scope.worldData = null; scope.worldData = null;
scope.worldNames = null; scope.worldNames = null;
scope.svg = null;
scope.g = null;
scope.ctrlKey = false; scope.ctrlKey = false;
//These are various options that should not be cached in scope.panel
scope.options = {
data: {
dropdown:[
{
"text": "Mercator (Flat)",
id: "mercator"
},
{
text: "Orthographic (Sphere)",
id: "orthographic"
}
]
}
};
//These should be moved to utility classes
var s4 = function() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
scope.uuid = s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
// Receive render events // Receive render events
scope.$on('render', function () { scope.$on('render', function () {
render_panel(); console.log("$on render");
if (typeof scope.svg === 'undefined') {
console.log("init");
init_panel();
} else {
console.log("render");
render_panel();
}
}); });
// Or if the window is resized // Or if the window is resized
angular.element(window).bind('resize', function () { angular.element(window).bind('resize', function () {
render_panel(); console.log("resize render");
if (typeof scope.svg === 'undefined') {
console.log("init");
init_panel();
} else {
console.log("render");
render_panel();
}
}); });
function render_panel() {
function init_panel() {
// 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?rand="+Math.floor(Math.random()*10000))
.script("panels/map2/lib/topojson.v1.min.js") .script("panels/map2/lib/topojson.v1.min.js?rand="+Math.floor(Math.random()*10000))
.script("panels/map2/lib/node-geohash.js") .script("panels/map2/lib/node-geohash.js?rand="+Math.floor(Math.random()*10000))
.script("panels/map2/lib/d3.hexbin.v0.min.js") .script("panels/map2/lib/d3.hexbin.v0.min.js?rand="+Math.floor(Math.random()*10000))
.script("panels/map2/lib/queue.v1.min.js") .script("panels/map2/lib/queue.v1.min.js?rand="+Math.floor(Math.random()*10000))
.script("panels/map2/display/binning.js") .script("panels/map2/display/binning.js?rand="+Math.floor(Math.random()*10000))
.script("panels/map2/display/geopoints.js") .script("panels/map2/display/geopoints.js?rand="+Math.floor(Math.random()*10000))
.script("panels/map2/display/bullseye.js"); .script("panels/map2/display/bullseye.js?rand="+Math.floor(Math.random()*10000));
// Populate element. Note that jvectormap appends, does not replace. // Populate element. Note that jvectormap appends, does not replace.
scripts.wait(function () { scripts.wait(function () {
elem.text('');
queue()
.defer(d3.json, "panels/map2/lib/world-110m.json")
.defer(d3.tsv, "panels/map2/lib/world-country-names.tsv")
//these files can take a bit of time to process, so save them in a variable .await(function(error, world, names) {
//and use those on redraw scope.worldData = world;
if (scope.worldData === null || scope.worldNames === null) { scope.worldNames = names;
queue()
.defer(d3.json, "panels/map2/lib/world-110m.json") console.log('initializing svg');
.defer(d3.tsv, "panels/map2/lib/world-country-names.tsv")
.await(function(error, world, names) {
scope.worldData = world;
scope.worldNames = names; //Better way to get these values? Seems kludgy to use jQuery on the div...
ready(); var width = $(elem[0]).width(),
}); height = $(elem[0]).height();
} else {
ready();
} //scale to whichever dimension is smaller, helps to ensure the whole map is displayed
scope.scale = (width > height) ? (height/5) : (width/5);
scope.zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", translate_map);
//used by choropleth
scope.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
scope.countries = topojson.feature(scope.worldData, scope.worldData.objects.countries).features;
scope.countries = scope.countries.filter(function(d) {
return scope.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);
});
//remove our old svg...is there a better way to update than remove/append?
//d3.select("#" + scope.uuid).select("svg").remove();
//create the new svg
scope.svg = d3.select(elem[0]).append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.call(scope.zoom);
scope.g = scope.svg.append("g");
//Overlay is used so that the entire map is draggable, not just the locations
//where countries are
scope.svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
console.log("finished initing");
render_panel();
});
}); });
} }
/**
* All map data has been loaded, go ahead and draw the map/data
*/
function ready() {
function render_panel() {
var world = scope.worldData,
names = scope.worldNames;
//Better way to get these values? Seems kludgy to use jQuery on the div... console.log("render_panel scope.svg", scope.svg);
var width = $(elem[0]).width(),
height = $(elem[0]).height();
//scale to whichever dimension is smaller, helps to ensure the whole map is displayed var width = $(elem[0]).width(),
var scale = (width > height) ? (height/5) : (width/5); height = $(elem[0]).height();
/** /**
* D3 and general config section * D3 and general config section
*/ */
var projection;
if (scope.panel.display.map.type === 'mercator') { scope.projection;
projection = d3.geo.mercator()
if (scope.panel.display.data.type === 'mercator') {
scope.projection = d3.geo.mercator()
.translate([width/2, height/2]) .translate([width/2, height/2])
.scale(scale); .scale(scope.scale);
} else if (scope.panel.display.map.type === 'orthographic') { } else if (scope.panel.display.data.type === 'orthographic') {
projection = d3.geo.orthographic() scope.projection = d3.geo.orthographic()
.scale(248) .translate([width/2, height/2])
.scale(100)
.clipAngle(90); .clipAngle(90);
var λ = d3.scale.linear() var λ = d3.scale.linear()
...@@ -288,76 +390,24 @@ angular.module('kibana.map2', []) ...@@ -288,76 +390,24 @@ angular.module('kibana.map2', [])
.range([90, -90]); .range([90, -90]);
} }
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8])
.on("zoom", move);
var path = d3.geo.path() var path = d3.geo.path()
.projection(projection); .projection(scope.projection);
//used by choropleth
var 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
var countries = topojson.feature(world, world.objects.countries).features;
countries = countries.filter(function(d) {
return names.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);
});
//Geocoded points are decoded into lat/lon, then projected onto x/y //Geocoded points are decoded into lat/lon, then projected onto x/y
points = _.map(scope.data, function (k, v) { scope.points = _.map(scope.data, function (k, v) {
var decoded = geohash.decode(v); var decoded = geohash.decode(v);
return [decoded.longitude, decoded.latitude]; return [decoded.longitude, decoded.latitude];
}); });
scope.projectedPoints = _.map(scope.points, function (k, v) {
return scope.projection(v);
});
/**
* D3 SVG Setup
*/
//set up some key listeners for our sphere dragging
window.focus();
d3.select(window)
.on("keydown", function() {
scope.ctrlKey = d3.event.ctrlKey;
})
.on("keyup", function() {
scope.ctrlKey = d3.event.ctrlKey;
});
//remove our old svg...is there a better way to update than remove/append?
d3.select(elem[0]).select("svg").remove();
//create the new svg
scope.svg = d3.select(elem[0]).append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.call(zoom);
scope.g = scope.svg.append("g");
//Overlay is used so that the entire map is draggable, not just the locations
//where countries are
scope.svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
//set up listener for ctrl key //set up listener for ctrl key
...@@ -365,12 +415,12 @@ angular.module('kibana.map2', []) ...@@ -365,12 +415,12 @@ angular.module('kibana.map2', [])
//Draw the countries, if this is a choropleth, draw with fancy colors //Draw the countries, if this is a choropleth, draw with fancy colors
scope.g.selectAll("path") scope.g.selectAll("countries")
.data(countries) .data(scope.countries)
.enter().append("path") .enter().append("path")
.attr("class", function(d) { .attr("class", function(d) {
if (scope.panel.display.choropleth.enabled) { if (scope.panel.display.choropleth.enabled) {
return 'land ' + quantize(scope.data[d.short]); return 'land ' + scope.quantize(scope.data[d.short]);
} else { } else {
return 'land'; return 'land';
} }
...@@ -379,13 +429,25 @@ angular.module('kibana.map2', []) ...@@ -379,13 +429,25 @@ angular.module('kibana.map2', [])
//draw boundaries //draw boundaries
scope.g.selectAll("land").append("path") scope.g.selectAll("land").append("path")
.datum(topojson.mesh(world, world.objects.land, function(a, b) { return a !== b; })) .datum(topojson.mesh(scope.worldData, scope.worldData.objects.land, function(a, b) { return a !== b; }))
.attr("class", "land boundary") .attr("class", "land boundary")
.attr("d", path); .attr("d", path);
if (scope.panel.display.map.type === 'orthographic') { if (scope.panel.display.data.type === 'orthographic') {
//set up some key listeners for our sphere dragging
window.focus();
d3.select(window)
.on("keydown", function() {
scope.ctrlKey = d3.event.ctrlKey;
})
.on("keyup", function() {
scope.ctrlKey = d3.event.ctrlKey;
});
scope.svg.style("cursor", "move") scope.svg.style("cursor", "move")
.call(d3.behavior.drag() .call(d3.behavior.drag()
.origin(function() { var rotate = projection.rotate(); return {x: 2 * rotate[0], y: -2 * rotate[1]}; }) .origin(function() { var rotate = projection.rotate(); return {x: 2 * rotate[0], y: -2 * rotate[1]}; })
...@@ -406,44 +468,48 @@ angular.module('kibana.map2', []) ...@@ -406,44 +468,48 @@ angular.module('kibana.map2', [])
if (scope.panel.display.binning.enabled) { if (scope.panel.display.binning.enabled) {
//@todo fix this //@todo fix this
var dimensions = [width, height]; var dimensions = [width, height];
displayBinning(scope, dimensions, projection, path); displayBinning(scope, dimensions, scope.projection, path);
} }
//Raw geopoints //Raw geopoints
if (scope.panel.display.geopoints.enabled) { //if (scope.panel.display.geopoints.enabled) {
displayGeopoints(scope, path); displayGeopoints(scope, path);
} //}
if (scope.panel.display.bullseye.enabled) { //if (scope.panel.display.bullseye.enabled) {
displayBullseye(scope, projection, path); displayBullseye(scope, scope.projection, path);
} //}
//d3.select(elem[0]).select(".loading").remove();
/** /**
* Zoom Functionality * Zoom Functionality
*/ */
if (scope.panel.display.scale != -1) { if (scope.panel.display.scale != -1) {
zoom.scale(scope.panel.display.scale).translate(scope.panel.display.translate); scope.zoom.scale(scope.panel.display.scale).translate(scope.panel.display.translate);
scope.g.style("stroke-width", 1 / scope.panel.display.scale).attr("transform", "translate(" + scope.panel.display.translate + ") scale(" + scope.panel.display.scale + ")"); scope.g.style("stroke-width", 1 / scope.panel.display.scale).attr("transform", "translate(" + scope.panel.display.translate + ") scale(" + scope.panel.display.scale + ")");
} }
function move() { }
if (! scope.ctrlKey) { function translate_map() {
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]));
zoom.translate(t);
scope.panel.display.translate = t; var width = $(elem[0]).width(),
scope.panel.display.scale = s; height = $(elem[0]).height();
scope.g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
}
}
if (! scope.ctrlKey) {
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]));
scope.zoom.translate(t);
scope.panel.display.translate = t;
scope.panel.display.scale = s;
scope.g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
}
} }
} }
}; };
......
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