From d3debd1f0962ea02c289a55486f283cd1eb0f049 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Wed, 10 Apr 2013 12:39:50 -0400 Subject: [PATCH] Big refactor, moving initialization code to own function, so that multiple graphs play nice together --- panels/map2/display/binning.js | 5 +- panels/map2/display/geopoints.js | 22 +- panels/map2/editor.html | 12 +- panels/map2/module.html | 9 +- panels/map2/module.js | 338 ++++++++++++++++++------------- 5 files changed, 238 insertions(+), 148 deletions(-) diff --git a/panels/map2/display/binning.js b/panels/map2/display/binning.js index 815d22e67c8..49dff5e1280 100644 --- a/panels/map2/display/binning.js +++ b/panels/map2/display/binning.js @@ -28,10 +28,7 @@ function displayBinning(scope, dimensions, projection, path) { } else { - binPoints = _.map(scope.data, function (k, v) { - var decoded = geohash.decode(v); - return projection([decoded.longitude, decoded.latitude]); - }); + binPoints = scope.projectedPoints; } //bin and sort the points, so we can set the various ranges appropriately diff --git a/panels/map2/display/geopoints.js b/panels/map2/display/geopoints.js index 7a123209db4..06956a82dd5 100644 --- a/panels/map2/display/geopoints.js +++ b/panels/map2/display/geopoints.js @@ -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 degrees = 180 / Math.PI - scope.g.selectAll("circles.points") - .data(points) - .enter().append("path") + var geopoints = scope.g.selectAll("geopoints") + .data(points); + + geopoints.enter().append("path") .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("class", "geopoint"); + geopoints.exit().remove(); + + + + + } \ No newline at end of file diff --git a/panels/map2/editor.html b/panels/map2/editor.html index d67fcea0264..fe53a1fd09e 100644 --- a/panels/map2/editor.html +++ b/panels/map2/editor.html @@ -61,7 +61,7 @@
@@ -87,7 +87,7 @@ @@ -216,7 +216,7 @@ + ng-model="panel.display.bullseye.enabled">{{panel.display.bullseye.enabled|enabledText}} @@ -259,6 +259,12 @@ value="{{panel.display.data.samples}}" /> + + Map Projection + + + +
diff --git a/panels/map2/module.html b/panels/map2/module.html index 660bf8cc792..0d6ce894cc4 100644 --- a/panels/map2/module.html +++ b/panels/map2/module.html @@ -7,11 +7,15 @@ .land { fill: #D1D1D1; + stroke: #595959; + stroke-linejoin: round; + stroke-linecap: round; + stroke-width: .1px; } .boundary { fill: none; - stroke: #fff; + stroke: #000; stroke-linejoin: round; stroke-linecap: round; } @@ -43,9 +47,12 @@ 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;} +
\ No newline at end of file diff --git a/panels/map2/module.js b/panels/map2/module.js index 3b028244657..cbe61eea6ed 100644 --- a/panels/map2/module.js +++ b/panels/map2/module.js @@ -15,16 +15,17 @@ angular.module('kibana.map2', []) translate:[0, 0], scale:-1, data: { - samples: 1000 + samples: 1000, + type: "mercator" }, geopoints: { - enabled: true, + enabled: false, enabledText: "Enabled", pointSize: 0.3, pointAlpha: 0.6 }, binning: { - enabled: true, + enabled: false, hexagonSize: 2, hexagonAlpha: 1.0, areaEncoding: true, @@ -41,12 +42,8 @@ angular.module('kibana.map2', []) lat: 0, lon: 0 } - }, - map: { - type: "mercator" } }, - displayTabs: ["Geopoints", "Binning", "Choropleth", "Bullseye", "Data"], activeDisplayTab:"Geopoints" }; @@ -188,95 +185,200 @@ angular.module('kibana.map2', []) .directive('map2', function () { return { restrict: 'A', + template: '
', link: function (scope, elem, attrs) { - elem.html('
') + + //elem.html('') scope.worldData = null; scope.worldNames = null; - scope.svg = null; - scope.g = null; + 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 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 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 - var scripts = $LAB.script("panels/map2/lib/d3.v3.min.js") - .script("panels/map2/lib/topojson.v1.min.js") - .script("panels/map2/lib/node-geohash.js") - .script("panels/map2/lib/d3.hexbin.v0.min.js") - .script("panels/map2/lib/queue.v1.min.js") - .script("panels/map2/display/binning.js") - .script("panels/map2/display/geopoints.js") - .script("panels/map2/display/bullseye.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?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 () { - elem.text(''); + + 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) { + scope.worldData = world; + scope.worldNames = names; + + console.log('initializing svg'); - //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() - .defer(d3.json, "panels/map2/lib/world-110m.json") - .defer(d3.tsv, "panels/map2/lib/world-country-names.tsv") - .await(function(error, world, names) { - scope.worldData = world; - scope.worldNames = names; - ready(); - }); - } else { - ready(); - } + //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 + 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() { + console.log("render_panel scope.svg", scope.svg); - var world = scope.worldData, - names = scope.worldNames; - //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 - var 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() + scope.projection; + + if (scope.panel.display.data.type === 'mercator') { + scope.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) + .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() @@ -288,76 +390,24 @@ angular.module('kibana.map2', []) .range([90, -90]); } - var zoom = d3.behavior.zoom() - .scaleExtent([1, 8]) - .on("zoom", move); - 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 - points = _.map(scope.data, function (k, v) { + 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); + }); - /** - * 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 @@ -365,12 +415,12 @@ angular.module('kibana.map2', []) //Draw the countries, if this is a choropleth, draw with fancy colors - scope.g.selectAll("path") - .data(countries) + scope.g.selectAll("countries") + .data(scope.countries) .enter().append("path") .attr("class", function(d) { if (scope.panel.display.choropleth.enabled) { - return 'land ' + quantize(scope.data[d.short]); + return 'land ' + scope.quantize(scope.data[d.short]); } else { return 'land'; } @@ -379,13 +429,25 @@ angular.module('kibana.map2', []) //draw boundaries 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("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") .call(d3.behavior.drag() .origin(function() { var rotate = projection.rotate(); return {x: 2 * rotate[0], y: -2 * rotate[1]}; }) @@ -406,44 +468,48 @@ angular.module('kibana.map2', []) if (scope.panel.display.binning.enabled) { //@todo fix this var dimensions = [width, height]; - displayBinning(scope, dimensions, projection, path); + displayBinning(scope, dimensions, scope.projection, path); } //Raw geopoints - if (scope.panel.display.geopoints.enabled) { + //if (scope.panel.display.geopoints.enabled) { displayGeopoints(scope, path); - } + //} - if (scope.panel.display.bullseye.enabled) { - displayBullseye(scope, projection, path); - } + //if (scope.panel.display.bullseye.enabled) { + displayBullseye(scope, scope.projection, path); + //} + + //d3.select(elem[0]).select(".loading").remove(); /** * Zoom Functionality */ 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 + ")"); } - function move() { + } - 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])); - zoom.translate(t); + function translate_map() { - scope.panel.display.translate = t; - scope.panel.display.scale = s; - scope.g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")"); - } + var width = $(elem[0]).width(), + height = $(elem[0]).height(); + + 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 + ")"); } - - } } };