1 /** 2 * Returns a geographic scale. The arguments to this constructor are optional, 3 * and equivalent to calling {@link #projection}. 4 * 5 * @class Represents a geographic scale; a mapping between latitude-longitude 6 * coordinates and screen pixel coordinates. By default, the domain is inferred 7 * from the geographic coordinates, so that the domain fills the output range. 8 * 9 * <p>Note that geographic scales are two-dimensional transformations, rather 10 * than the one-dimensional bidrectional mapping typical of other scales. 11 * Rather than mapping (for example) between a numeric domain and a numeric 12 * range, geographic scales map between two coordinate objects: {@link 13 * pv.Geo.LatLng} and {@link pv.Vector}. 14 * 15 * @param {pv.Geo.Projection} [p] optional projection. 16 * @see pv.Geo.scale#ticks 17 */ 18 pv.Geo.scale = function(p) { 19 var rmin = {x: 0, y: 0}, // default range minimum 20 rmax = {x: 1, y: 1}, // default range maximum 21 d = [], // default domain 22 j = pv.Geo.projections.identity, // domain <-> normalized range 23 x = pv.Scale.linear(-1, 1).range(0, 1), // normalized <-> range 24 y = pv.Scale.linear(-1, 1).range(1, 0), // normalized <-> range 25 c = {lng: 0, lat: 0}, // Center Point 26 lastLatLng, // cached latlng 27 lastPoint; // cached point 28 29 /** @private */ 30 function scale(latlng) { 31 if (!lastLatLng 32 || (latlng.lng != lastLatLng.lng) 33 || (latlng.lat != lastLatLng.lat)) { 34 lastLatLng = latlng; 35 var p = project(latlng); 36 lastPoint = {x: x(p.x), y: y(p.y)}; 37 } 38 return lastPoint; 39 } 40 41 /** @private */ 42 function project(latlng) { 43 var offset = {lng: latlng.lng - c.lng, lat: latlng.lat}; 44 return j.project(offset); 45 } 46 47 /** @private */ 48 function invert(xy) { 49 var latlng = j.invert(xy); 50 latlng.lng += c.lng; 51 return latlng; 52 } 53 54 /** Returns the projected x-coordinate. */ 55 scale.x = function(latlng) { 56 return scale(latlng).x; 57 }; 58 59 /** Returns the projected y-coordinate. */ 60 scale.y = function(latlng) { 61 return scale(latlng).y; 62 }; 63 64 /** 65 * Abstract; this is a local namespace on a given geographic scale. 66 * 67 * @namespace Tick functions for geographic scales. Because geographic scales 68 * represent two-dimensional transformations (as opposed to one-dimensional 69 * transformations typical of other scales), the tick values are similarly 70 * represented as two-dimensional coordinates in the input domain, i.e., 71 * {@link pv.Geo.LatLng} objects. 72 * 73 * <p>Also, note that non-rectilinear projections, such as sinsuoidal and 74 * aitoff, may not produce straight lines for constant longitude or constant 75 * latitude. Therefore the returned array of ticks is a two-dimensional array, 76 * sampling various latitudes as constant longitude, and vice versa. 77 * 78 * <p>The tick lines can therefore be approximated as polylines, either with 79 * "linear" or "cardinal" interpolation. This is not as accurate as drawing 80 * the true curve through the projection space, but is usually sufficient. 81 * 82 * @name pv.Geo.scale.prototype.ticks 83 * @see pv.Geo.scale 84 * @see pv.Geo.LatLng 85 * @see pv.Line#interpolate 86 */ 87 scale.ticks = { 88 89 /** 90 * Returns longitude ticks. 91 * 92 * @function 93 * @param {number} [m] the desired number of ticks. 94 * @returns {array} a nested array of <tt>pv.Geo.LatLng</tt> ticks. 95 * @name pv.Geo.scale.prototype.ticks.prototype.lng 96 */ 97 lng: function(m) { 98 var lat, lng; 99 if (d.length > 1) { 100 var s = pv.Scale.linear(); 101 if (m == undefined) m = 10; 102 lat = s.domain(d, function(d) { return d.lat; }).ticks(m); 103 lng = s.domain(d, function(d) { return d.lng; }).ticks(m); 104 } else { 105 lat = pv.range(-80, 81, 10); 106 lng = pv.range(-180, 181, 10); 107 } 108 return lng.map(function(lng) { 109 return lat.map(function(lat) { 110 return {lat: lat, lng: lng}; 111 }); 112 }); 113 }, 114 115 /** 116 * Returns latitude ticks. 117 * 118 * @function 119 * @param {number} [m] the desired number of ticks. 120 * @returns {array} a nested array of <tt>pv.Geo.LatLng</tt> ticks. 121 * @name pv.Geo.scale.prototype.ticks.prototype.lat 122 */ 123 lat: function(m) { 124 return pv.transpose(scale.ticks.lng(m)); 125 } 126 }; 127 128 /** 129 * Inverts the specified value in the output range, returning the 130 * corresponding value in the input domain. This is frequently used to convert 131 * the mouse location (see {@link pv.Mark#mouse}) to a value in the input 132 * domain. Inversion is only supported for numeric ranges, and not colors. 133 * 134 * <p>Note that this method does not do any rounding or bounds checking. If 135 * the input domain is discrete (e.g., an array index), the returned value 136 * should be rounded. If the specified <tt>y</tt> value is outside the range, 137 * the returned value may be equivalently outside the input domain. 138 * 139 * @function 140 * @name pv.Geo.scale.prototype.invert 141 * @param {number} y a value in the output range (a pixel location). 142 * @returns {number} a value in the input domain. 143 */ 144 scale.invert = function(p) { 145 return invert({x: x.invert(p.x), y: y.invert(p.y)}); 146 }; 147 148 /** 149 * Sets or gets the input domain. Note that unlike quantitative scales, the 150 * domain cannot be reduced to a simple rectangle (i.e., minimum and maximum 151 * values for latitude and longitude). Instead, the domain values must be 152 * projected to normalized space, effectively finding the domain in normalized 153 * space rather than in terms of latitude and longitude. Thus, changing the 154 * projection requires recomputing the normalized domain. 155 * 156 * <p>This method can be invoked several ways: 157 * 158 * <p>1. <tt>domain(values...)</tt> 159 * 160 * <p>Specifying the domain as a series of {@link pv.Geo.LatLng}s is the most 161 * explicit and recommended approach. However, if the domain values are 162 * derived from data, you may find the second method more appropriate. 163 * 164 * <p>2. <tt>domain(array, f)</tt> 165 * 166 * <p>Rather than enumerating the domain explicitly, you can specify a single 167 * argument of an array. In addition, you can specify an optional accessor 168 * function to extract the domain values (as {@link pv.Geo.LatLng}s) from the 169 * array. If the specified array has fewer than two elements, this scale will 170 * default to the full normalized domain. 171 * 172 * <p>2. <tt>domain()</tt> 173 * 174 * <p>Invoking the <tt>domain</tt> method with no arguments returns the 175 * current domain as an array. 176 * 177 * @function 178 * @name pv.Geo.scale.prototype.domain 179 * @param {...} domain... domain values. 180 * @returns {pv.Geo.scale} <tt>this</tt>, or the current domain. 181 */ 182 scale.domain = function(array, f) { 183 if (arguments.length) { 184 d = (array instanceof Array) 185 ? ((arguments.length > 1) ? pv.map(array, f) : array) 186 : Array.prototype.slice.call(arguments); 187 if (d.length > 1) { 188 var lngs = d.map(function(c) { return c.lng; }); 189 var lats = d.map(function(c) { return c.lat; }); 190 c = { 191 lng: (pv.max(lngs) + pv.min(lngs)) / 2, 192 lat: (pv.max(lats) + pv.min(lats)) / 2 193 }; 194 var n = d.map(project); // normalized domain 195 x.domain(n, function(p) { return p.x; }); 196 y.domain(n, function(p) { return p.y; }); 197 } else { 198 c = {lng: 0, lat: 0}; 199 x.domain(-1, 1); 200 y.domain(-1, 1); 201 } 202 lastLatLng = null; // invalidate the cache 203 return this; 204 } 205 return d; 206 }; 207 208 /** 209 * Sets or gets the output range. This method can be invoked several ways: 210 * 211 * <p>1. <tt>range(min, max)</tt> 212 * 213 * <p>If two objects are specified, the arguments should be {@link pv.Vector}s 214 * which specify the minimum and maximum values of the x- and y-coordinates 215 * explicitly. 216 * 217 * <p>2. <tt>range(width, height)</tt> 218 * 219 * <p>If two numbers are specified, the arguments specify the maximum values 220 * of the x- and y-coordinates explicitly; the minimum values are implicitly 221 * zero. 222 * 223 * <p>3. <tt>range()</tt> 224 * 225 * <p>Invoking the <tt>range</tt> method with no arguments returns the current 226 * range as an array of two {@link pv.Vector}s: the minimum (top-left) and 227 * maximum (bottom-right) values. 228 * 229 * @function 230 * @name pv.Geo.scale.prototype.range 231 * @param {...} range... range values. 232 * @returns {pv.Geo.scale} <tt>this</tt>, or the current range. 233 */ 234 scale.range = function(min, max) { 235 if (arguments.length) { 236 if (typeof min == "object") { 237 rmin = {x: Number(min.x), y: Number(min.y)}; 238 rmax = {x: Number(max.x), y: Number(max.y)}; 239 } else { 240 rmin = {x: 0, y: 0}; 241 rmax = {x: Number(min), y: Number(max)}; 242 } 243 x.range(rmin.x, rmax.x); 244 y.range(rmax.y, rmin.y); // XXX flipped? 245 lastLatLng = null; // invalidate the cache 246 return this; 247 } 248 return [rmin, rmax]; 249 }; 250 251 /** 252 * Sets or gets the projection. This method can be invoked several ways: 253 * 254 * <p>1. <tt>projection(string)</tt> 255 * 256 * <p>Specifying a string sets the projection to the given named projection in 257 * {@link pv.Geo.projections}. If no such projection is found, the identity 258 * projection is used. 259 * 260 * <p>2. <tt>projection(object)</tt> 261 * 262 * <p>Specifying an object sets the projection to the given custom projection, 263 * which must implement the <i>forward</i> and <i>inverse</i> methods per the 264 * {@link pv.Geo.Projection} interface. 265 * 266 * <p>3. <tt>projection()</tt> 267 * 268 * <p>Invoking the <tt>projection</tt> method with no arguments returns the 269 * current object that defined the projection. 270 * 271 * @function 272 * @name pv.Scale.geo.prototype.projection 273 * @param {...} range... range values. 274 * @returns {pv.Scale.geo} <tt>this</tt>, or the current range. 275 */ 276 scale.projection = function(p) { 277 if (arguments.length) { 278 j = typeof p == "string" 279 ? pv.Geo.projections[p] || pv.Geo.projections.identity 280 : p; 281 return this.domain(d); // recompute normalized domain 282 } 283 return p; 284 }; 285 286 /** 287 * Returns a view of this scale by the specified accessor function <tt>f</tt>. 288 * Given a scale <tt>g</tt>, <tt>g.by(function(d) d.foo)</tt> is equivalent to 289 * <tt>function(d) g(d.foo)</tt>. This method should be used judiciously; it 290 * is typically more clear to invoke the scale directly, passing in the value 291 * to be scaled. 292 * 293 * @function 294 * @name pv.Geo.scale.prototype.by 295 * @param {function} f an accessor function. 296 * @returns {pv.Geo.scale} a view of this scale by the specified accessor 297 * function. 298 */ 299 scale.by = function(f) { 300 function by() { return scale(f.apply(this, arguments)); } 301 for (var method in scale) by[method] = scale[method]; 302 return by; 303 }; 304 305 if (arguments.length) scale.projection(p); 306 return scale; 307 }; 308