(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.mapmap = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o. */ 'use strict'; // test whether in a browser environment if (typeof window === 'undefined') { // node var d3dsv = require('d3-dsv'); var fs = require('fs'); var fileparser = function(func) { return function(path, row, callback) { if (dd.isUndefined(callback)) { callback = row; row = null; } fs.readFile(path, 'utf8', function(error, data) { if (error) return callback(error); data = func(data, row); callback(null,data); }); }; }; var d3 = { csv: fileparser(d3dsv.csv.parse), tsv: fileparser(d3dsv.tsv.parse), json: fileparser(JSON.parse) }; } else { // browser // we expect global d3 to be available var d3 = window.d3; } function rowFileHandler(loader) { // TODO: file handler API should not need to be passed map, reduce functions but be wrapped externally return function(path, map, reduce, options) { options = dd.merge({ // default accessor function tries to convert number-like strings to numbers accessor: function(d) { var keys = Object.keys(d); for (var i=0; i map to attribute value map = dd.map.key(map); } if (dd.isString(spec)) { // consider spec to be a URL/file to load var handler = options.fileHandler || getFileHandler(options.type || spec); if (handler) { return handler(spec, map, reduce, options); } else { throw new Error("datadata.js: Unknown file type for: " + spec); } } if (dd.isArray(spec)) { return new Promise(function(resolve, reject) { resolve(dd.mapreduce(spec, map, reduce)); }); } throw new Error("datadata.js: Unknown data specification."); }; // expose registration method & rowFileHandler helper dd.registerFileHandler = registerFileHandler; dd.rowFileHandler = rowFileHandler; // simple load function, returns a promise for data without map/reduce-ing // DO NOT USE - present only for mapmap.js legacy reasons dd.load = function(spec, key) { if (spec.then && typeof spec.then === 'function') { // already a thenable / promise return spec; } else if (dd.isString(spec)) { // consider spec to be a URL to load // guess type var ext = spec.split('.').pop(); if (ext == 'json' || ext == 'topojson' || ext == 'geojson') { return new Promise(function(resolve, reject) { d3.json(spec, function(error, data) { if (error) { reject(error); return; } resolve(data); }); }); } else { console.warn("Unknown extension: " + ext); } } }; // Type checking /** Return true if argument is a string. @param {any} val - The value to check. */ dd.isString = function (val) { return Object.prototype.toString.call(val) == '[object String]'; }; /** Return true if argument is a function. @param {any} val - The value to check. */ dd.isFunction = function(obj) { return (typeof obj === 'function'); }; /** Return true if argument is an Array. @param {any} val - The value to check. */ dd.isArray = function(obj) { return (obj instanceof Array); }; /** Return true if argument is an Object, but not an Array, String or anything created with a custom constructor. @param {any} val - The value to check. */ dd.isDictionary = function(obj) { return (obj && obj.constructor && obj.constructor === Object); }; /** Return true if argument is undefined. @param {any} val - The value to check. */ dd.isUndefined = function(obj) { return (typeof obj == 'undefined'); }; /** Return true if argument is a number or a string that strictly looks like a number. This method is stricter than +val or parseInt(val) as it doesn't validate the empty string or strings contining any non-numeric characters. @param {any} val - The value to check. */ dd.isNumeric = function(val) { // check if string looks like a number // +"" => 0 // parseInt("") => NaN // parseInt("123OK") => 123 // +"123OK" => NaN // so we need to pass both to be strict return !isNaN(+val) && !isNaN(parseFloat(val)); } // Type conversion / utilities /** If the argument is already an Array, return a copy of the Array. Else, return a single-element Array containing the argument. */ dd.toArray = function(val) { if (!val) return []; // return a copy if aready array, else single-element array return dd.isArray(val) ? val.slice() : [val]; }; /** Shallow object merging, mainly for options. Returns a new object. */ dd.merge = function() { var obj = {}; for (var i = 0; i < arguments.length; i++) { var src = arguments[i]; for (var key in src) { if (src.hasOwnProperty(key)) { obj[key] = src[key]; } } } return obj; }; /** Return an {@link module:datadata.OrderedHash|OrderedHash} object. @exports module:datadata.OrderedHash */ dd.OrderedHash = function() { // ordered hash implementation var keys = []; var vals = {}; return { /** Add a key/value pair to the end of the OrderedHash. @param {String} k - Key @param v - Value */ push: function(k,v) { if (!vals[k]) keys.push(k); vals[k] = v; }, /** Insert a key/value pair at the specified position. @param {Number} i - Index to insert value at @param {String} k - Key @param v - Value */ insert: function(i,k,v) { if (!vals[k]) { keys.splice(i,0,k); vals[k] = v; } }, /** Return the value for specified key. @param {String} k - Key */ get: function(k) { // string -> key return vals[k]; }, /** Return the value at specified index position. @param {String} i - Index */ at: function(i) { // number -> nth object return vals[keys[i]]; }, length: function(){return keys.length;}, keys: function(){return keys;}, key: function(i) {return keys[i];}, values: function() { return keys.map(function(key){return vals[key];}); }, map: function(func) { return keys.map(function(k){return func(k, vals[k]);}); }, unsorted_dict: function() { return vals; } }; }; // Utility functions for map/reduce dd.map = { key: function(attr, remap) { return function(d, emit) { var key = d[attr]; if (remap && remap[key] !== undefined) { key = remap[key]; } emit(key, d); }; }, dict: function(dict) { return function(d, emit) { emit(d, dict[d]); }; } }; dd.emit = { ident: function() { return function(key, values, emit) { emit(key, values); }; }, first: function() { return function(key, values, emit) { emit(key, values[0]); }; }, last: function() { return function(key, values, emit) { emit(key, values[values.length - 1]); }; }, merge: function() { return function(key, values, emit) { var obj = values.reduce(function(prev, curr) { var keys = Object.keys(curr); for (var i=0; i -1) { doAdd = true; break; } } for (j=0; j -1) { doAdd = false; break; } } if (doAdd && prev[key] && curr[key] && !isNaN(prev[key]) && !isNaN(curr[key])) { prev[key] = prev[key] + curr[key]; } else { prev[key] = curr[key]; if (doAdd) { console.warn("datadata.emit.sum(): Cannot add keys " + key + "!"); } } } return prev; }); emit(key, obj); }; } }; dd.map.geo = { point: function(latProp, lonProp, keyProp) { var id = 0; return function(d, emit) { var key = keyProp ? d[keyProp] : id++; emit(key, dd.geo.Point(d[lonProp], d[latProp], d)); }; } }; dd.emit.geo = { segments: function() { return function(key, data, emit) { var prev = null, cur = null; for (var i=0; i. */ var dd = require('datadata'); var version = '0.2.8'; function assert(test, message) { if (test) return; throw new Error("[mapmap] " + message);} assert(window.d3, "d3.js is required!"); assert(window.Promise, "Promises not available in your browser - please add the necessary polyfill, as detailed in https://github.com/floledermann/mapmap.js#using-mapmapjs"); var default_settings = { locale: 'en', keepAspectRatio: true, placeholderClassName: 'placeholder', svgAttributes: { 'overflow': 'hidden' // needed for IE }, pathAttributes: { 'fill': 'none', 'stroke': '#000', 'stroke-width': '0.2', 'stroke-linejoin': 'bevel', 'pointer-events': 'none' }, backgroundAttributes: { 'width': '300%', 'height': '300%', 'fill': 'none', 'stroke': 'none', 'transform': 'translate(-800,-400)', 'pointer-events': 'all' }, overlayAttributes: { 'fill': '#ffffff', 'fill-opacity': '0.2', 'stroke-width': '0.8', 'stroke': '#333', 'pointer-events': 'none' }, defaultMetadata: { // domain: is determined by data analysis scale: 'quantize', colors: ["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#2c7fb8","#253494"], // Colorbrewer YlGnBu[6] undefinedValue: "", //"undefined" //undefinedLabel: -> from locale undefinedColor: 'transparent' } }; var mapmap = function(element, options) { // ensure constructor invocation if (!(this instanceof mapmap)) return new mapmap(element, options); this.settings = {}; this.options(mapmap.extend({}, default_settings, options)); // promises this._promise = { geometry: null, data: null }; this.selected = null; this.layers = new dd.OrderedHash(); //this.identify_func = identify_layer; this.identify_func = identify_by_properties(); this.metadata_specs = []; // convert selector expression to node element = d3.select(element).node(); // defaults this.projection(d3.geo.mercator().scale(1)); this.initEngine(element); this.initEvents(element); this.dispatcher = d3.dispatch('choropleth','view','click','mousedown','mouseup','mousemove'); return this; }; // expose datadata library in case we are bundled for browser // (browserify doesn't support mutliple global exports) mapmap.datadata = dd; mapmap.prototype = { version: version }; mapmap.extend = function extend(){ for(var i=1; i 0) { this.supports.hoverDomModification = false; } else { this.supports.hoverDomModification = true; } // Firefox < 35 will report wrong BoundingClientRect (adding clipped background), // https://bugzilla.mozilla.org/show_bug.cgi?id=530985 var match = /Firefox\/(\d+)/.exec(navigator.userAgent); if (match && parseInt(match[1]) < 35) { this.supports.svgGetBoundingClientRect = false; } else { this.supports.svgGetBoundingClientRect = true; } var map = this; // save viewport state separately, as zoom may not have exact values (due to animation interpolation) this.current_scale = 1; this.current_translate = [0,0]; this.zoom = d3.behavior.zoom() .translate([0, 0]) .scale(1) .scaleExtent([1, 8]) .on('zoom', function () { map.current_scale = d3.event.scale; map.current_translate = d3.event.translate; mapEl.attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'); if (!map.supports.nonScalingStroke) { //map._elements.geometry.selectAll("path").style("stroke-width", 1.5 / d3.event.scale + "px"); } }); mapEl //.call(this.zoom) // free mousewheel zooming .call(this.zoom.event); /* var drag = d3.behavior.drag() .origin(function() {return {x:map.current_translate[0],y:map.current_translate[1]};}) .on('dragstart', function() { d3.event.sourceEvent.stopPropagation(); }) .on('dragend', function() { d3.event.sourceEvent.stopPropagation(); }) .on('drag', function() { map.current_translate = [d3.event.x, d3.event.y]; mapEl.attr('transform', 'translate(' + d3.event.x + ',' + d3.event.y + ')scale(' + map.current_scale + ')'); }) ;*/ //mapEl.call(drag); var map = this; function constructEvent(event) { // TODO: maybe this should be offsetX/Y, but then we need to change // zoomToViewportPosition to support click-to-zoom var pos = [event.clientX, event.clientY], location = map._projection.invert ? map._projection.invert(pos) : null; return { position: pos, location: map._projection.invert(pos), event: event } }; mapEl.on('click', function() { // TODO: check if anyone is listening, else return immediately map.dispatcher.click.call(map, constructEvent(d3.event)); }); mapEl.on('mousedown', function() { // TODO: check if anyone is listening, else return immediately map.dispatcher.mousedown.call(map, constructEvent(d3.event)); }); mapEl.on('mouseup', function() { // TODO: check if anyone is listening, else return immediately map.dispatcher.mousedown.call(map, constructEvent(d3.event)); }); mapEl.on('mousemove', function() { // TODO: check if anyone is listening, else return immediately map.dispatcher.mousedown.call(map, constructEvent(d3.event)); }); }; mapmap.prototype.getGeometryPane = function() { return this._elements.geometry; } mapmap.prototype.getOverlayPane = function() { return this._elements.overlay; } mapmap.prototype.getFixedPane = function() { return this._elements.fixed; } mapmap.prototype.initEvents = function(element) { var map = this; // keep aspect ratio on resize function resize() { map.bounds = map.getBoundingClientRect(); if (map.settings.keepAspectRatio) { var width = element.getAttribute('width'), height = element.getAttribute('height'); if (width && height && map.bounds.width) { var ratio = width / height; element.style.height = (map.bounds.width / ratio) + 'px'; } } } window.onresize = resize; resize(); }; var domain = [0,1]; var layer_counter = 0; // TODO: think about caching loaded resources (#8) mapmap.prototype.geometry = function(spec, keyOrOptions) { // key is default option var options = dd.isString(keyOrOptions) ? {key: keyOrOptions} : keyOrOptions; options = dd.merge({ key: 'id', setExtent: true // layers: taken from input or auto-generated layer name }, options); var map = this; if (dd.isFunction(spec)) { this._promise.geometry.then(function(topo){ var new_topo = spec(topo); if (typeof new_topo.length == 'undefined') { new_topo = [new_topo]; } new_topo.map(function(t) { if (typeof t.geometry.length == 'undefined') { t.geometry = [t.geometry]; } if (typeof t.index == 'undefined') { map.layers.push(t.name, t.geometry); } else { map.layers.insert(t.index, t.name, t.geometry); } }); if (options.setExtent) { if (!map.selected_extent) { map._extent(spec); } map.draw(); if (options.ondraw) options.ondraw(); } }); return this; } if (dd.isDictionary(spec)) { if (!options.layers) { options.layers = 'layer-' + layer_counter++; } spec = [{type:'Feature',geometry:spec}]; map.layers.push(options.layers, spec); // add dummy promise, we are not loading anything var promise = new Promise(function(resolve, reject) { resolve(spec); }); this.promise_data(promise); // set up projection first to avoid reprojecting geometry // TODO: setExtent options should be decoupled from drawing, // we need a way to defer both until drawing on last geom promise works if (options.setExtent) { if (!map.selected_extent) { map._extent(spec); } map.draw(); if (options.ondraw) options.ondraw(); } return this; } if (dd.isArray(spec)) { // Array case var new_topo = dd.mapreduce(spec, options.map, options.reduce); if (!options.layers) { options.layers = 'layer-' + layer_counter++; } map.layers.push(options.layers, new_topo.values()); // add dummy promise, we are not loading anything var promise = new Promise(function(resolve, reject) { resolve(new_topo); }); this.promise_data(promise); // set up projection first to avoid reprojecting geometry if (options.setExtent) { if (!map.selected_extent) { map._extent(new_topo.values()); } // TODO: we need a smarter way of setting up projection/bounding box initially // if extent() was called, this should have set up bounds, else we need to do it here // however, extent() currently operates on the rendered s generated by draw() // Also: draw should be called only at end of promise chain, not inbetween! //this._promise.geometry.then(draw); map.draw(); if (options.ondraw) options.ondraw(); } return this; } var promise = dd.load(spec); // chain to existing geometry promise if (this._promise.geometry) { var parent = this._promise.geometry; this._promise.geometry = new Promise(function(resolve, reject) { parent.then(function(_) { promise.then(function(data) { resolve(data); }); }); }); } else { this._promise.geometry = promise; } this._promise.geometry.then(function(geom) { if (geom.type && geom.type == 'Topology') { // TopoJSON var keys = options.layers || Object.keys(geom.objects); keys.map(function(k) { if (geom.objects[k]) { var objs = topojson.feature(geom, geom.objects[k]).features; map.layers.push(k, objs); // TODO: support functions for map as well as strings if (options.key) { for (var i=0; is generated by draw() //this._promise.geometry.then(draw); map.draw(); if (options.ondraw) options.ondraw(); }); // put into chained data promise to make sure is loaded before later data // note this has to happen after merging into this._promise.geometry to make // sure layers are created first (e.g. for highlighting) this.promise_data(promise); return this; }; var identify_by_properties = function(properties){ // TODO: calling this without properties should use primary key as property // however, this is not stored in the object's properties currently // so there is no easy way to access it if (!properties) { properties = '__key__'; } // single string case if (properties.substr) { properties = [properties]; } return function(layers, name){ name = name.toString().toLowerCase(); // layers have priority, so iterate them first var lyr = layers.get(name); if (lyr) return lyr; var result = []; // properties are ordered by relevance, so iterate these first for (var k=0; k -1) { left += parseInt(cs.left.slice(0,-2)); } // this tests getBoundingClientRect() to be non-buggy if (bounds.left == left - scrollLeft) { return bounds; } // construct synthetic boundingbox from computed style var top = parentOffset.top, width = parseInt(cs.width.slice(0,-2)), height = parseInt(cs.height.slice(0,-2)); return { left: left - scrollLeft, top: top - scrollTop, width: width, height: height, right: left + width - scrollLeft, bottom: top + height - scrollTop }; }; // TODO: disable pointer-events for not selected paths mapmap.prototype.select = function(selection) { var map = this; function getName(sel) { return (typeof sel == 'string') ? sel : (sel.selectionName || 'function'); } var oldSel = this.selected; if (this.selected) { this._elements.main.classed('selected-' + getName(this.selected), false); } this.selected = selection; if (this.selected) { this._elements.main.classed('selected-' + getName(this.selected), true); } this.promise_data().then(function(){ if (oldSel) { map.getRepresentations(oldSel).classed('selected',false); } if (selection) { map.getRepresentations(selection).classed('selected',true); } }); return this; }; mapmap.prototype.highlight = function(selection) { var map = this; if (selection === null) { map._elements.shadowEl.selectAll('path').remove(); map._elements.shadowCropEl.selectAll('path').remove(); } else { this.promise_data().then(function(data) { var obj = map.getRepresentations(selection); map._elements.shadowEl.selectAll('path').remove(); map._elements.shadowCropEl.selectAll('path').remove(); obj.each(function() { map._elements.shadowEl.append('path') .attr({ d: this.attributes.d.value, fill: 'rgba(0,0,0,0.5)' //'#999' }); map._elements.shadowCropEl.append('path') .attr({ d: this.attributes.d.value, fill: '#fff' }); }); }); } return this; }; /* Call without parameters to get current selection. Call with null to get all topology objects. Call with function to filter geometries. Call with string to filter geometries/layers based on identify(). Call with geometry to convert into d3 selection. Returns a D3 selection. */ mapmap.prototype.getRepresentations = function(selection) { if (typeof selection == 'undefined') { selection = this.selected; } if (selection) { if (typeof selection == 'function') { return this._elements.geometry.selectAll('path').filter(function(d,i){ return selection(d.properties); }); } if (selection.__data__) { // is a geometry generated by d3 -> return selection return d3.select(selection); } // TODO: this should have a nicer API var obj = this.identify_func(this.layers, selection); if (!obj) return d3.select(null); // layer case if (obj.length) { return d3.selectAll(obj.map(function(d){return d.__repr__;})); } // object case return d3.select(obj.__repr__); } return this._elements.geometry.selectAll('path'); }; // TODO: this is an ugly hack for now, until we properly keep track of current merged data! mapmap.prototype.getData = function(key, selection) { var map = this; return new Promise(function(resolve, reject) { map._promise.data.then(function(data) { data = dd.OrderedHash(); map.getRepresentations(selection)[0].forEach(function(d){ if (typeof d.__data__.properties[key] != 'undefined') { data.push(d.__data__.properties[key], d.__data__.properties); } }); resolve(data); }); }); }; mapmap.prototype.promise_data = function(promise) { // chain a new promise to the data promise // this allows a more elegant API than Promise.all([promises]) // since we use only a single promise the "encapsulates" the // previous ones // TODO: hide this._promise.data through a closure? // TODO: we only fulfill with most recent data - should // we not *always* fulfill with canonical data i.e. the // underlying selection, or keep canonical data and refresh // selection always? // Also, we need to keep data that has no entities in the geometry // e.g. for loading stats of aggregated entities. We could // use a global array of GeoJSON features, as this allows // either geometry or properties to be null -- fl 2015-11-21 var map = this; if (promise) { if (this._promise.data) { this._promise.data = new Promise(function(resolve, reject) { map._promise.data.then(function(_) { promise.then(function(data) { resolve(data); }); }); }); } else { this._promise.data = promise; } } return this._promise.data; }; mapmap.prototype.then = function(callback) { this.promise_data().then(callback); return this; }; // TODO: think about caching loaded resources (#8) mapmap.prototype.data = function(spec, keyOrOptions) { var options = dd.isDictionary(keyOrOptions) ? keyOrOptions : {map: keyOrOptions}; options = dd.merge({ geometryKey: '__key__' // natural key // map: datdata default // reduce: datdata default }, options); var map = this; if (typeof spec == 'function') { this.promise_data().then(function(data){ // TODO: this is a mess, see above - data // doesn't contain the actual canonical data, but // only the most recently requested one, which doesn't // help us for transformations map._elements.geometry.selectAll('path') .each(function(geom) { if (geom.properties) { var val = spec(geom.properties); if (val) { mapmap.extend(geom.properties, val); } } }); }); } else { this.promise_data(dd(spec, options.map, options.reduce, options)) .then(function(data) { if (data.length() == 0) { console.warn("Data for key '" + options.map + "' yielded no results!"); } map._elements.geometry.selectAll('path') .each(function(d) { if (d.properties) { var k = d.properties[options.geometryKey]; if (k) { mapmap.extend(d.properties, data.get(k)); } else { //console.warn("Key '" + options.geometryKey + "' not found in " + this + "!"); } } }); }); } return this; }; var MetaDataSpec = function(key, fields) { // ensure constructor invocation if (!(this instanceof MetaDataSpec)) return new MetaDataSpec(key, fields); mapmap.extend(this, fields); this.key = key; return this; }; MetaDataSpec.prototype.specificity = function() { // regex case. use length of string representation without enclosing /.../ if (this.key instanceof RegExp) return this.key.toString()-2; // return number of significant letters return this.key.length - (this.key.match(/[\*\?]/g) || []).length; }; MetaDataSpec.prototype.match = function(str) { if (this.key instanceof RegExp) return (str.search(this.key) == 0); var rex = new RegExp('^' + this.key.replace('*','.*').replace('?','.')); return (str.search(rex) == 0); }; var MetaData = function(fields, localeProvider) { // ensure constructor invocation if (!(this instanceof MetaData)) return new MetaData(fields, localeProvider); mapmap.extend(this, fields); // take default from locale if (this.undefinedLabel === undefined) this.undefinedLabel = localeProvider.locale.undefinedLabel; this.format = function(val) { if (!this._format) { this._format = this.getFormatter(); } // return undefined if undefined or if not a number but number formatting explicitly requested if (val === undefined || val === null || (this.numberFormat && (isNaN(val)))) { return this.undefinedValue; } return this._format(val); }; this.getFormatter = function() { if (this.scale == 'ordinal' && this.valueLabels) { var scale = d3.scale.ordinal().domain(this.domain).range(this.valueLabels); return scale; } if (this.numberFormat && typeof this.numberFormat == 'function') { return this.numberFormat; } if (localeProvider.locale) { return localeProvider.locale.numberFormat(this.numberFormat || '.01f'); } return d3.format(this.numberFormat || '.01f'); }; this.getRangeFormatter = function() { var fmt = this.format.bind(this); return function(lower, upper, excludeLower, excludeUpper) { if (localeProvider.locale && localeProvider.locale.rangeLabel) { return localeProvider.locale.rangeLabel(lower, upper, fmt, excludeLower, excludeUpper); } return defaultRangeLabel(lower, upper, fmt, excludeLower, excludeUpper); } }; return this; }; mapmap.prototype.meta = function(metadata){ var keys = Object.keys(metadata); for (var i=0; i stats.max) stats.max = val; if (val > 0) stats.anyPositive = true; if (val < 0) stats.anyNegative = true; } else if (val) { stats.anyString = true; } } } if (data.each && typeof data.each == 'function') { data.each(datumFunc); } else { for (var i=0; i -1) ? metadata.domain[i] : null; } } return scale; }; mapmap.prototype.autoLinearScale = function(valueFunc) { var stats = getStats(this._elements.geometry.selectAll('path'), properties_accessor(valueFunc)); return d3.scale.linear() .domain([0,stats.max]); }; mapmap.prototype.autoSqrtScale = function(valueFunc) { var stats = getStats(this._elements.geometry.selectAll('path'), properties_accessor(valueFunc)); return d3.scale.sqrt() .domain([0,stats.max]); }; mapmap.prototype.attr = function(name, value, selection) { if (dd.isDictionary(name) && value) { selection = value; value = undefined; } this.symbolize(function(repr) { // d3 checks for arguments.length, so we need to explicitly distinguish // dictionary and name/value cases if (value === undefined) { repr.attr(name); } else { repr.attr(name, value); } }, selection); return this; }; mapmap.prototype.zOrder = function(comparator, options) { options = dd.merge({ undefinedValue: Infinity }, options); if (dd.isString(comparator)) { var fieldName = comparator; var reverse = false; if (fieldName[0] == "-") { reverse = true; fieldName = fieldName.substring(1); } comparator = function(a,b) { var valA = a.properties[fieldName], valB = b.properties[fieldName]; if (!dd.isNumeric(valA)) { valA = options.undefinedValue; } if (!dd.isNumeric(valB)) { valB = options.undefinedValue; } var result = valA - valB; if (reverse) result *= -1; return result; } } var map = this; this.promise_data().then(function(data) { map.getRepresentations() .sort(comparator); }); return this; }; mapmap.prototype.symbolize = function(callback, selection, finalize) { var map = this; // store in closure for later access selection = selection || this.selected; this.promise_data().then(function(data) { map.getRepresentations(selection) .each(function(geom) { callback.call(map, d3.select(this), geom, geom.properties); }); if (finalize) finalize.call(map); }); return this; }; mapmap.prototype.symbolizeAttribute = function(property, reprAttribute, options) { options = dd.merge({ //metadata: null, // taken from map metadata metaAttribute: reprAttribute, selection: this.selected, legend: true }, options); var defaultUndefinedAttributes = { 'stroke': 'transparent' }; var valueFunc = keyOrCallback(property); var map = this; this.promise_data().then(function(data) { var metadata = options.metadata; if (dd.isString(metadata)) { metadata = map.getMetadata(metadata); } if (!metadata) { metadata = options.metadata || map.getMetadata(property); } var scale = d3.scale[metadata.scale](); scale.domain(metadata.domain).range(metadata[reprAttribute]); map.symbolize(function(el, geom, data) { el.attr(reprAttribute, function(geom) { var val = valueFunc(geom.properties); if (val == null || (metadata.scale != 'ordinal' && isNaN(val))) { return (metadata.undefinedSymbols && metadata.undefinedSymbols[reprAttribute]) || defaultUndefinedAttributes[reprAttribute]; } return scale(val); }); }, options.selection); if (options.legend) { map.updateLegend(property, reprAttribute, metadata, scale, options.selection); } }); return this; } // legacy method - this has moved to the mapmap.symbolize namespace mapmap.prototype.choropleth = function(spec, metadata, selection) { this.symbolize(mapmap.symbolize.choropleth(spec, metadata, selection), selection, function(){ this.dispatcher.choropleth.call(this, spec); }); return this; } mapmap.symbolize = {}; // TODO: improve handling of using a function here vs. using a named property // probably needs a unified mechanism to deal with property/func to be used elsewhere mapmap.symbolize.choropleth = function(spec, metadata, selection) { // we have to remember the scale for legend() var colorScale = null, valueFunc = keyOrCallback(spec), map = this; function color(el, geom, data) { if (spec === null) { // clear el.attr('fill', this.settings.pathAttributes.fill); return; } // on first call, set up scale & legend if (!colorScale) { // TODO: improve handling of things that need the data, but should be performed // only once. Should we provide a separate callback for this, or use the // promise_data().then() for setup? As this could be considered a public API usecase, // maybe using promises is a bit steep for outside users? if (typeof metadata == 'string') { metadata = this.getMetadata(metadata); } if (!metadata) { metadata = this.getMetadata(spec); } colorScale = this.autoColorScale(spec, metadata, selection); this.updateLegend(spec, 'fill', metadata, colorScale, selection); } if (el.attr('fill') != 'none') { // transition if color already set el = el.transition(); } el.attr('fill', function(geom) { var val = valueFunc(geom.properties); // check if value is undefined or null if (val == null || (metadata.scale != 'ordinal' && isNaN(val))) { return metadata.undefinedColor || map.settings.pathAttributes.fill; } return colorScale(val) || map.settings.pathAttributes.fill; }); } return color; }; mapmap.symbolize.addLabel = function(spec, textAttributes) { textAttributes = dd.merge({ stroke: '#ffffff', fill: '#000000', 'font-size': 9, 'paint-order': 'stroke fill', 'alignment-baseline': 'middle', 'text-anchor': 'middle', dx: 0, dy: 1 }, textAttributes); var valueFunc = keyOrCallback(spec); return function(el, geom, data) { if (spec === null) { this.getOverlayPane().select('text').remove(); return; } if (geom.properties && typeof valueFunc(geom.properties) != 'undefined') { var centroid = this.getPathGenerator().centroid(geom); this.getOverlayPane().append('text') .text(valueFunc(geom.properties)) .attr(textAttributes) .attr({ x: centroid[0], y: centroid[1] }) ; } } } mapmap.symbolize.addTitle = addOptionalElement('title'); mapmap.symbolize.addDesc = addOptionalElement('desc'); function addOptionalElement(elementName) { return function(value) { var valueFunc = keyOrCallback(value); return function(el, d) { if (value === null) { el.select(elementName).remove(); return; } el.append(elementName) .text(valueFunc(d.properties)); }; }; } var center = { x: 0.5, y: 0.5 }; mapmap.prototype.center = function(center_x, center_y) { center.x = center_x; if (typeof center_y != 'undefined') { center.y = center_y; } return this; }; // store all hover out callbacks here, this will be called on zoom var hoverOutCallbacks = []; function callHoverOut() { for (var i=0; i mapBounds.right - options.clipMargins.right) pt.x = mapBounds.right - options.clipMargins.right; if (pt.y < mapBounds.top + options.clipMargins.top) pt.y = mapBounds.top + options.clipMargins.top; if (pt.y > mapBounds.bottom - options.clipMargins.bottom) pt.y = mapBounds.bottom - options.clipMargins.bottom; } pt.x -= mapBounds.left; pt.y -= mapBounds.top; return pt; } mapmap.prototype.getAnchorForMousePosition = function(event, repr, options) { options = dd.merge({ anchorOffset: [0,-20] }, options); // http://www.jacklmoore.com/notes/mouse-position/ var offsetX = event.layerX || event.offsetX, offsetY = event.layerY || event.offsetY; return { x: offsetX + options.anchorOffset[0], y: offsetY + options.anchorOffset[1] } } mapmap.prototype.hover = function(overCB, outCB, options) { options = dd.merge({ moveToFront: true, clipToViewport: true, clipMargins: {top: 40, left: 40, bottom: 0, right: 40}, selection: null, anchorPosition: this.getAnchorForRepr, hoverRepresentationStyle: { 'pointer-events': 'visiblePainted' } }, options); var map = this; if (!this._oldPointerEvents) { this._oldPointerEvents = []; } this.promise_data().then(function() { var obj = map.getRepresentations(options.selection); mouseover = function(d) { // "this" is the SVG element, not the map! // move to top = end of parent node // this screws up IE event handling! if (options.moveToFront && map.supports.hoverDomModification) { // TODO: this should be solved via a second element to be placed in front! this.__hoverinsertposition__ = this.nextSibling; this.parentNode.appendChild(this); } var el = this, event = d3.event; var sel = d3.select(this); this._oldstyle = sel.attr('style'); sel.style(options.hoverRepresentationStyle); // In Firefox the event positions are not populated properly in some cases // Defer call to allow browser to populate the event window.setTimeout(function(){ var anchor = options.anchorPosition.call(map, event, el, options); overCB.call(map, d.properties, anchor, el); }, 10); }; // reset previously overridden pointer events for (var i=0; i' : ''; post = (i==0) ? '
' : '
'; if (typeof part == 'function') { var str = part.call(map, d); if (str) { html += pre + str + post; } continue; } var meta = map.getMetadata(part); var prefix = meta.hoverLabel || meta.valueLabel || meta.label || ''; if (prefix) prefix += ": "; var val = meta.format(d[part]); if (val == 'NaN') val = d[part]; // TODO: make option "ignoreUndefined" etc. if (val !== undefined && val !== meta.undefinedValue) { html += pre + prefix + val + ( meta.valueUnit ? ' ' + meta.valueUnit : '') + post; } else if (meta.undefinedLabel) { html += pre + prefix + meta.undefinedLabel + post; } } } return html; }; return func; }; mapmap.prototype.hoverInfo = function(spec, options) { options = dd.merge({ selection: null, hoverClassName: 'hoverInfo', hoverStyle: { position: 'absolute', padding: '0.5em 0.7em', 'background-color': 'rgba(255,255,255,0.85)', // avoid clipping DIV to right edge of map 'white-space': 'nowrap', 'z-index': '2' }, hoverEnterStyle: { display: 'block' }, hoverLeaveStyle: { display: 'none' } }, options); var hoverEl = this._elements.parent.select('.' + options.hoverClassName); if (!spec) { return this.hover(null, null, options); } var htmlFunc = this.buildHTMLFunc(spec); if (hoverEl.empty()) { hoverEl = this._elements.parent.append('div').attr('class',options.hoverClassName); } hoverEl.style(options.hoverStyle); if (!hoverEl.mapmap_eventHandlerInstalled) { hoverEl.on('mouseenter', function() { hoverEl.style(options.hoverEnterStyle); }).on('mouseleave', function() { hoverEl.style(options.hoverLeaveStyle); }); hoverEl.mapmap_eventHandlerInstalled = true; } function show(d, point){ var html = htmlFunc(d); if (html) { // offsetParent only works for rendered objects, so place object first! // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement.offsetParent hoverEl.style(options.hoverEnterStyle); var offsetEl = hoverEl.node().offsetParent || hoverEl.node(), mainEl = this._elements.main.node(), bounds = this.getBoundingClientRect(), offsetBounds = offsetEl.getBoundingClientRect(), scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0, scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0, top = bounds.top - offsetBounds.top, left = bounds.left - offsetBounds.left; if (getComputedStyle(offsetEl).getPropertyValue('position') == 'static') { console.warn("mapmap.js Warning: hoverInfo parent element needs to be positioned (relative or absolute) for positioning to work properly!"); } hoverEl .style({ bottom: (offsetBounds.height - top - point.y) + 'px', //top: point.y + 'px', left: (left + point.x) + 'px' }) .html(html); } } function hide() { hoverEl.style(options.hoverLeaveStyle); } return this.hover(show, hide, options); }; // remove all symbology // TODO: symbolizers should be registered somehow and iterated over here mapmap.prototype.clear = function() { this.choropleth(null); this.proportional_circles(null); this.title(null); this.desc(null); return this; }; // namespace for re-usable behaviors mapmap.behavior = {}; mapmap.behavior.zoom = function(options) { options = dd.merge({ event: 'click', cursor: 'pointer', fitScale: 0.7, animationDuration: 750, maxZoom: 8, hierarchical: false, showRing: true, ringRadius: 1.1, // relative to height/2 zoomstart: null, zoomend: null, center: [center.x, center.y], ringAttributes: { stroke: '#000', 'stroke-width': 6, 'stroke-opacity': 0.3, 'pointer-events': 'none', fill: 'none' }, closeButton: function(parent) { parent.append('circle') .attr({ r: 10, fill: '#fff', stroke: '#000', 'stroke-width': 2.5, 'stroke-opacity': 0.9, 'fill-opacity': 0.9, cursor: 'pointer' }); parent.append('text') .attr({ 'text-anchor':'middle', cursor: 'pointer', 'font-weight': 'bold', 'font-size': '18', y: 6 }) .text('×'); }, // TODO: how should highlighting work on the map generally? // maybe more like setState('highlight') and options.activestyle = 'highlight' ? activate: function(el) { d3.select(el).classed('active', true); }, deactivate: function(el) { if (el) d3.select(el).classed('active', false); } }, options); var ring = null, map = null, r, r0, zoomed = null; var z = function(selection) { map = this; var size = this.size(); r = Math.min(size.height, size.width) / 2.0 * options.ringRadius; r0 = Math.sqrt(size.width*size.width + size.height*size.height) / 1.5; if (options.cursor) { selection.attr({ cursor: options.cursor }); } if (options.showRing && !ring) { ring = map.getFixedPane().selectAll('g.zoomRing') .data([1]); var newring = ring.enter() .append('g') .attr('class','zoomRing') .attr('transform','translate(' + size.width * options.center[0] + ',' + size.height * options.center[1] + ')'); newring.append('circle') .attr('class', 'main') .attr('r', r0) .attr(options.ringAttributes); var close = newring.append('g') .attr('class','zoomOut') .attr('transform','translate(' + (r0 * 0.707) + ',-' + (r0 * 0.707) + ')'); if (options.closeButton) { options.closeButton(close); } } // this is currently needed if e.g. search zooms to somewhere else, // but map is still zoomed in through this behavior // do a reset(), but without modifying the map view (=zooming out) map.on('view', function(translate, scale) { if (zoomed && scale == 1) { zoomed = null; animateRing(null); map._elements.map.select('.background').on(options.event + '.zoom', null); options.zoomstart && options.zoomstart.call(map, null); options.zoomend && options.zoomend.call(map, null); } }); selection.on(options.event, function(d) { callHoverOut(); if (zoomed == this) { reset(); } else { options.deactivate(zoomed); var el = this; options.zoomstart && options.zoomstart.call(map, el); map.zoomToSelection(this, { callback: function() { options.zoomend && options.zoomend.call(map, el); }, maxZoom: options.maxZoom, center: options.center }); animateRing(this); options.activate(this); zoomed = this; map._elements.map.select('.background').on(options.event + '.zoom', reset); } }); if (zoomed) { options.zoomstart && options.zoomstart.call(map, zoomed); options.zoomend && options.zoomend.call(map, zoomed); } }; function zoomTo(selection) { options.zoomstart && options.zoomstart.call(map, selection); map.zoomToSelection(selection, { callback: function() { options.zoomend && options.zoomend.call(map, selection); }, maxZoom: options.maxZoom, center: options.center }); animateRing(selection); zoomed = selection; map._elements.map.select('.background').on(options.event + '.zoom', reset); } function animateRing(selection) { if (ring) { var new_r = (selection) ? r : r0; ring.select('circle.main').transition().duration(options.animationDuration) .attr({ r: new_r }) ; ring.select('g.zoomOut').transition().duration(options.animationDuration) .attr('transform', 'translate(' + (new_r * 0.707) + ',-' + (new_r * 0.707) + ')'); // sqrt(2) / 2 // caveat: make sure to assign this every time to apply correct closure if we have multiple zoom behaviors!! ring.select('g.zoomOut').on('click', reset); } } function reset() { if (map) { options.deactivate(zoomed); zoomed = null; map.resetZoom(); animateRing(null); map._elements.map.select('.background').on(options.event + '.zoom', null); if (options.zoomstart) { options.zoomstart.call(map, null); } if (options.zoomend) { options.zoomend.call(map, null); } } } z.reset = reset; z.active = function() { return zoomed; }; z.remove = function() { reset(); }; z.from = function(other){ if (other && other.active) { zoomed = other.active(); /* if (zoomed) { zoomTo(zoomed); } */ // TODO: make up our mind whether this should remove the other behavior // in burgenland_demographie.html, we need to keep it as it would otherwise zoom out // but if we mix different behaviors, we may want to remove the other one automatically // (or maybe require it to be done manually) // in pendeln.js, we remove the other behavior here, which is inconsistent! //other.remove(); } return z; }; return z; }; mapmap.prototype.animateView = function(translate, scale, callback, duration) { duration = duration || 750; if (translate[0] == this.current_translate[0] && translate[1] == this.current_translate[1] && scale == this.current_scale) { // nothing to do // yield to simulate async callback if (callback) { window.setTimeout(callback, 10); } return this; } this.current_translate = translate; this.current_scale = scale; callHoverOut(); var map = this; this._elements.map.transition() .duration(duration) .call(map.zoom.translate(translate).scale(scale).event) .each('start', function() { map._elements.shadowGroup.attr('display','none'); }) .each('end', function() { map._elements.shadowGroup.attr('display','block'); if (callback) { callback(); } }) .each('interrupt', function() { map._elements.shadowGroup.attr('display','block'); // not sure if we should call callback here, but it may be non-intuitive // for callback to never be called if zoom is cancelled if (callback) { callback(); } }); this.dispatcher.view.call(this, translate, scale); return this; }; mapmap.prototype.setView = function(translate, scale) { translate = translate || this.current_translate; scale = scale || this.current_scale; this.current_translate = translate; this.current_scale = scale; // do we need this? //callHoverOut(); this.zoom.translate(translate).scale(scale).event(this._elements.map); this.dispatcher.view.call(this, translate, scale); return this; }; mapmap.prototype.getView = function() { return { translate: this.current_translate, scale: this.current_scale } }; mapmap.prototype.zoomToSelection = function(selection, options) { options = dd.merge({ fitScale: 0.7, animationDuration: 750, maxZoom: 8, center: [center.x, center.y] }, options); var sel = this.getRepresentations(selection), bounds = [[Infinity,Infinity],[-Infinity, -Infinity]], pathGenerator = this.getPathGenerator(); sel.each(function(el){ var b = pathGenerator.bounds(el); bounds[0][0] = Math.min(bounds[0][0], b[0][0]); bounds[0][1] = Math.min(bounds[0][1], b[0][1]); bounds[1][0] = Math.max(bounds[1][0], b[1][0]); bounds[1][1] = Math.max(bounds[1][1], b[1][1]); }); var dx = bounds[1][0] - bounds[0][0], dy = bounds[1][1] - bounds[0][1], x = (bounds[0][0] + bounds[1][0]) / 2, y = (bounds[0][1] + bounds[1][1]) / 2, size = this.size(), scale = Math.min(options.maxZoom, options.fitScale / Math.max(dx / size.width, dy / size.height)), translate = [size.width * options.center[0] - scale * x, size.height * options.center[1] - scale * y]; this.animateView(translate, scale, options.callback, options.animationDuration); return this; }; mapmap.prototype.zoomToBounds = function(bounds, callback, duration) { var w = bounds[1][0]-bounds[0][0], h = bounds[1][1]-bounds[0][1], cx = (bounds[1][0]+bounds[0][0]) / 2, cy = (bounds[1][1]+bounds[0][1]) / 2, size = this.size(), scale = Math.min(2, 0.9 / Math.max(w / size.width, h / size.height)), translate = [size.width * 0.5 - scale * cx, size.height * 0.5 - scale * cy]; return this.animateView(translate, scale, callback, duration); }; mapmap.prototype.zoomToCenter = function(center, scale, callback, duration) { scale = scale || 1; var size = this.size(), translate = [size.width * 0.5 - scale * center[0], size.height * 0.5 - scale * center[1]]; return this.animateView(translate, scale, callback, duration); }; mapmap.prototype.zoomToViewportPosition = function(center, scale, callback, duration) { var point = this._elements.svg.node().createSVGPoint(); point.x = center[0]; point.y = center[1]; var ctm = this._elements.geometry.node().getScreenCTM().inverse(); point = point.matrixTransform(ctm); point = [point.x, point.y]; scale = scale || 1; //var point = [(center[0]-this.current_translate[0])/this.current_scale, (center[1]-this.current_translate[1])/this.current_scale]; return this.zoomToCenter(point, scale, callback, duration); }; mapmap.prototype.resetZoom = function(callback, duration) { return this.animateView([0,0],1, callback, duration); // TODO take center into account zoomed-out, we may not always want this? //doZoom([width * (center.x-0.5),height * (center.y-0.5)],1); }; // Manipulate representation geometry. This can be used e.g. to register event handlers. // spec is a function to be called with selection to set up event handler mapmap.prototype.applyBehavior = function(spec, selection) { assert(dd.isFunction(spec), "Behavior must be a function"); var map = this; this._promise.geometry.then(function(topo) { var sel = map.getRepresentations(selection); // TODO: this should be configurable via options // and needs to integrate with managing pointer events (see hoverInfo) sel.style('pointer-events','visiblePainted'); spec.call(map, sel); }); return this; }; // apply a behavior on the whole map pane (e.g. drag/zoom etc.) mapmap.prototype.applyMapBehavior = function(spec) { spec.call(this, this._elements.map); return this; }; // deprecated methods using UK-spelling mapmap.prototype.applyBehaviour = function(spec, selection) { console && console.log && console.log("Deprecation warning: applyBehaviour() is deprecated, use applyBehavior() (US spelling) instead!"); return this.applyBehavior(spec, selection); } mapmap.prototype.applyMapBehaviour = function(spec, selection) { console && console.log && console.log("Deprecation warning: applyMapBehaviour() is deprecated, use applyMapBehavior() (US spelling) instead!"); return this.applyMapBehavior(spec, selection); } // handler for high-level events on the map object mapmap.prototype.on = function(eventName, handler) { this.dispatcher.on(eventName, handler); return this; }; function defaultRangeLabel(lower, upper, format, excludeLower, excludeUpper) { var f = format || function(lower){return lower}; if (isNaN(lower)) { if (isNaN(upper)) { console.warn("rangeLabel: neither lower nor upper value specified!"); return ""; } else { return (excludeUpper ? "under " : "up to ") + f(upper); } } if (isNaN(upper)) { return excludeLower ? ("more than " + f(lower)) : (f(lower) + " and more"); } return (excludeLower ? '> ' : '') + f(lower) + " to " + (excludeUpper ? '<' : '') + f(upper); } var d3_locales = { 'en': { decimal: ".", thousands: ",", grouping: [ 3 ], currency: [ "$", "" ], dateTime: "%a %b %e %X %Y", date: "%m/%d/%Y", time: "%H:%M:%S", periods: [ "AM", "PM" ], days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], shortDays: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], shortMonths: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ], rangeLabel: defaultRangeLabel, undefinedLabel: "no data" }, 'de': { decimal: ",", thousands: ".", grouping: [3], currency: ["€", ""], dateTime: "%a %b %e %X %Y", date: "%d.%m.%Y", time: "%H:%M:%S", periods: ["AM", "PM"], days: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"], shortDays: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], months: ["Jänner", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], shortMonths: ["Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sep.", "Okt.", "Nov.", "Dez."], rangeLabel: function(lower, upper, format, excludeLower, excludeUpper) { var f = format || function(lower){return lower}; if (isNaN(lower)) { if (isNaN(upper)) { console.warn("rangeLabel: neither lower nor upper value specified!"); return ""; } else { return (excludeUpper ? "unter " : "bis ") + f(upper); } } if (isNaN(upper)) { return (excludeLower ? "mehr als " + f(lower) : f(lower) + " und mehr"); } return (excludeLower ? '> ' : '') + f(lower) + " bis " + (excludeUpper ? '<' : '') + f(upper); }, undefinedLabel: "keine Daten" } }; mapmap.prototype.setLocale = function(lang){ var locale; if (dd.isString(lang) && d3_locales[lang]) { locale = d3_locales[lang]; } else { locale = lang; } this.locale = d3.locale(locale); // D3's locale doesn't support extended attributes, // so copy them over manually var keys = Object.keys(locale); for (var i=0; i 0 && width < 1) width = 1; return width; }; }); var legend_func = function(attribute, reprAttribute, metadata, classes, undefinedClass) { var legend = this._elements.parent.select('.' + options.legendClassName); if (legend.empty()) { legend = this._elements.parent.append('div') .attr('class',options.legendClassName); } legend.style(options.legendStyle); // TODO: attribute may be a function, so we cannot easily generate a label for it var title = legend.selectAll('h3') .data([valueOrCall(metadata.label, attribute) || (dd.isString(attribute) ? attribute : '')]); title.enter().append('h3'); title.html(function(d){return d;}); // we need highest values first for numeric scales if (metadata.scale != 'ordinal') { classes.reverse(); } if (options.reverse) { classes.reverse(); } if (undefinedClass) { classes.push(undefinedClass); } if (options.order) { assert(dd.isFunction(options.order), "LegendOptions.order must be a comparator function!"); classes.sort(options.order); } var cells = legend.selectAll('div.legendCell') .data(classes); cells.exit().remove(); var newcells = cells.enter() .append('div') .style(options.cellStyle); cells .attr('class', 'legendCell') .each(function(d) { if (d.class) { d3.select(this).classed(d.class, true); } }); function updateRepresentations(newcells, cells, options) { newcells = newcells.append('svg') .attr('class', 'legendColor') .style(options.colorBoxStyle); if (reprAttribute == 'fill') { newcells.append('rect') .attr({ width: 100, height: 100 }) .attr({ 'fill': function(d) {return d.representation;} }); cells.select('.legendColor rect') .transition() .attr({ 'fill': function(d) {return d.representation;} }); } else if (reprAttribute == 'stroke') { // construct attributes object from reprAttribute variable var strokeAttrs = {}; strokeAttrs[reprAttribute] = function(d) {return d.representation;}; newcells.append('line') .attr({ y1: 10, y2: 10, x1: 5, x2: 100, stroke: '#000000', 'stroke-width': 3 }) .attr(strokeAttrs); cells.select('.legendColor rect') .transition() .attr(strokeAttrs); } } updateRepresentations(newcells, cells, options); newcells.append('span') .attr('class','legendLabel') .style(options.labelStyle); cells.attr('data-count',function(d) {return d.count();}); cells.select('.legendLabel') .text(function(d) { var formatter; // TODO: we need some way of finding out whether we have intervals or values from the metadata // to cache the label formatter if (d.valueRange) { formatter = metadata.getRangeFormatter(); return formatter(d.valueRange[0], d.valueRange[1], d.includeLower, d.includeUpper); } if (d.value) { formatter = metadata.getFormatter(); return formatter(d.value); } return metadata.undefinedLabel; }); if (options.histogram) { newcells.append('span') .attr('class', 'legendHistogramBar') .style(options.histogramBarStyle); cells.select('.legendHistogramBar').transition() .style('width', function(d){ var width = options.histogramBarWidth(d.count()); // string returned? -> use unchanged if (width.length && width.indexOf('px') == width.lenght - 2) { return width; } return Math.round(width) + 'px'; }) .text(function(d) { return ' ' + d.count(); }); } if (options.callback) options.callback(); } legend_func.clear = function() { this._elements.parent.select('.' + options.legendClassName).remove(); } return legend_func; } mapmap.prototype.projection = function(projection) { if (projection === undefined) return this._projection; this._projection = projection; this._pathGenerator = d3.geo.path().projection(projection); return this; } mapmap.prototype.project = function(point) { return this._projection(point); }; mapmap.prototype.getPathGenerator = function() { return this._pathGenerator; } mapmap.prototype.extent = function(selection, options) { var map = this; this.selected_extent = selection || this.selected; this._promise.geometry.then(function(topo) { // TODO: getRepresentations() depends on s being drawn, but we want to // be able to call extent() before draw() to set up projection // solution: manage merged geometry + data independent from SVG representation var geom = map.getRepresentations(map.selected_extent); var all = { 'type': 'FeatureCollection', 'features': [] }; geom.each(function(d){ all.features.push(d); }); map._extent(all, options); }); return this; }; mapmap.prototype._extent = function(geom, options) { options = dd.merge({ fillFactor: 0.9 }, options); // convert/merge topoJSON if (geom.type && geom.type == 'Topology') { // we need to merge all named features var names = Object.keys(geom.objects); var all = []; for (var i=0; i