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,124 +185,130 @@ angular.module('kibana.map2', []) ...@@ -188,124 +185,130 @@ 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 () {
console.log("$on render");
if (typeof scope.svg === 'undefined') {
console.log("init");
init_panel();
} else {
console.log("render");
render_panel(); 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 () {
console.log("resize render");
if (typeof scope.svg === 'undefined') {
console.log("init");
init_panel();
} else {
console.log("render");
render_panel(); 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('');
//these files can take a bit of time to process, so save them in a variable
//and use those on redraw
if (scope.worldData === null || scope.worldNames === null) {
queue() queue()
.defer(d3.json, "panels/map2/lib/world-110m.json") .defer(d3.json, "panels/map2/lib/world-110m.json")
.defer(d3.tsv, "panels/map2/lib/world-country-names.tsv") .defer(d3.tsv, "panels/map2/lib/world-country-names.tsv")
.await(function(error, world, names) { .await(function(error, world, names) {
scope.worldData = world; scope.worldData = world;
scope.worldNames = names; scope.worldNames = names;
ready();
});
} else {
ready();
}
});
}
/**
* All map data has been loaded, go ahead and draw the map/data
*/
function ready() {
console.log('initializing svg');
var world = scope.worldData,
names = scope.worldNames;
//Better way to get these values? Seems kludgy to use jQuery on the div... //Better way to get these values? Seems kludgy to use jQuery on the div...
var width = $(elem[0]).width(), var width = $(elem[0]).width(),
height = $(elem[0]).height(); height = $(elem[0]).height();
//scale to whichever dimension is smaller, helps to ensure the whole map is displayed //scale to whichever dimension is smaller, helps to ensure the whole map is displayed
var scale = (width > height) ? (height/5) : (width/5); scope.scale = (width > height) ? (height/5) : (width/5);
/**
* D3 and general config section
*/
var projection;
if (scope.panel.display.map.type === 'mercator') {
projection = d3.geo.mercator()
.translate([width/2, height/2])
.scale(scale);
} else if (scope.panel.display.map.type === 'orthographic') {
projection = d3.geo.orthographic()
.scale(248)
.clipAngle(90);
var λ = d3.scale.linear()
.domain([0, width])
.range([-180, 180]);
var φ = d3.scale.linear() scope.zoom = d3.behavior.zoom()
.domain([0, height])
.range([90, -90]);
}
var zoom = d3.behavior.zoom()
.scaleExtent([1, 8]) .scaleExtent([1, 8])
.on("zoom", move); .on("zoom", translate_map);
var path = d3.geo.path()
.projection(projection);
//used by choropleth //used by choropleth
var quantize = d3.scale.quantize() scope.quantize = d3.scale.quantize()
.domain([0, 1000]) .domain([0, 1000])
.range(d3.range(9).map(function(i) { return "q" + (i+1); })); .range(d3.range(9).map(function(i) { return "q" + (i+1); }));
//Extract name and two-letter codes for our countries //Extract name and two-letter codes for our countries
var countries = topojson.feature(world, world.objects.countries).features; scope.countries = topojson.feature(scope.worldData, scope.worldData.objects.countries).features;
countries = countries.filter(function(d) { scope.countries = scope.countries.filter(function(d) {
return names.some(function(n) { return scope.worldNames.some(function(n) {
if (d.id == n.id) { if (d.id == n.id) {
d.name = n.name; d.name = n.name;
return d.short = n.short; return d.short = n.short;
...@@ -315,38 +318,18 @@ angular.module('kibana.map2', []) ...@@ -315,38 +318,18 @@ angular.module('kibana.map2', [])
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
//Geocoded points are decoded into lat/lon, then projected onto x/y
points = _.map(scope.data, function (k, v) {
var decoded = geohash.decode(v);
return [decoded.longitude, decoded.latitude];
});
/**
* 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? //remove our old svg...is there a better way to update than remove/append?
d3.select(elem[0]).select("svg").remove(); //d3.select("#" + scope.uuid).select("svg").remove();
//create the new svg //create the new svg
scope.svg = d3.select(elem[0]).append("svg") scope.svg = d3.select(elem[0]).append("svg")
.attr("width", width) .attr("width", width)
.attr("height", height) .attr("height", height)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")") .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.call(zoom); .call(scope.zoom);
scope.g = scope.svg.append("g"); scope.g = scope.svg.append("g");
...@@ -360,17 +343,84 @@ angular.module('kibana.map2', []) ...@@ -360,17 +343,84 @@ angular.module('kibana.map2', [])
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
console.log("finished initing");
render_panel();
});
});
}
function render_panel() {
console.log("render_panel scope.svg", scope.svg);
var width = $(elem[0]).width(),
height = $(elem[0]).height();
/**
* D3 and general config section
*/
scope.projection;
if (scope.panel.display.data.type === 'mercator') {
scope.projection = d3.geo.mercator()
.translate([width/2, height/2])
.scale(scope.scale);
} else if (scope.panel.display.data.type === 'orthographic') {
scope.projection = d3.geo.orthographic()
.translate([width/2, height/2])
.scale(100)
.clipAngle(90);
var λ = d3.scale.linear()
.domain([0, width])
.range([-180, 180]);
var φ = d3.scale.linear()
.domain([0, height])
.range([90, -90]);
}
var path = d3.geo.path()
.projection(scope.projection);
//Geocoded points are decoded into lat/lon, then projected onto x/y
scope.points = _.map(scope.data, function (k, v) {
var decoded = geohash.decode(v);
return [decoded.longitude, decoded.latitude];
});
scope.projectedPoints = _.map(scope.points, function (k, v) {
return scope.projection(v);
});
//set up listener for ctrl key //set up listener for ctrl key
//scope.svg //scope.svg
//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,45 +468,49 @@ angular.module('kibana.map2', []) ...@@ -406,45 +468,49 @@ 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() { }
function translate_map() {
var width = $(elem[0]).width(),
height = $(elem[0]).height();
if (! scope.ctrlKey) { if (! scope.ctrlKey) {
var t = d3.event.translate, var t = d3.event.translate,
s = d3.event.scale; s = d3.event.scale;
t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0])); 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])); t[1] = Math.min(height / 2 * (s - 1) + 230 * s, Math.max(height / 2 * (1 - s) - 230 * s, t[1]));
zoom.translate(t); scope.zoom.translate(t);
scope.panel.display.translate = t; scope.panel.display.translate = t;
scope.panel.display.scale = s; scope.panel.display.scale = s;
scope.g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")"); scope.g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
} }
} }
}
} }
}; };
}); });
\ No newline at end of file
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