Customizable Wagner Projections using d3-geo
– Seven Of Nine

Between 1932 and 1949, Karlheinz Wagner presented nine map projections. Each of them is customizable, i.e. they can easily be modified in certain regards to meet the wishes and needs of the user. Unfortunately, these options have been largely ignored in cartographic literature, with a few exceptions.
The projections nowadays known as Wagner I to IX are merely examples, special cases
that Wagner found particularly advantageous.
Here, I’m showing a way to employ all possible instances of seven of the nine projections
using d3-geo-projection.
A description of the functions and their parameters as well as the reason for differences in certain parameters is given in the blogpost “More Umbeziffern for d3-geo-projections”.
For general information about Wagner’s transformation method, read this blogpost or the notes at the Wagner-Variationen-Generator (WVG-7) and in the article Das Umbeziffern – Wagners Transformation Method.
Examples
Each of the examples shows a single instance of a customizable Wagner projection to convey an idea of the possibilities. They also present the source code that generates the image. This might be helpful for people who haven’t worked with d3-geo-projection before.
In the interactive demo, you can render all possible instances of a certain customizable Wagner projection by dragging a few sliders.
Except for the customizable Wagner VII/VIII, all variants are PROPOSALS.
There might be bugs. There might be changes. They might never make it into the
official release of d3-geo-projection.
-
The customizable Wagner I/II.
A pseudocylindric projection with sinusoidal meridians. You can adjust the length of the pole line, the aspect ratio of the axes and the amount of areal inflation (incl. setting it to zero which makes it an equal-area projection).
For informations about the parameters and their notation, read this blogpost.
➡ Example #1 | Example #2 | Interactive Demo -
The customizable Wagner III.
A pseudocylindric projection with equally spaced parallels. You can adjust the length of the pole line, the curvature of parallels and the standard parallel.
I recommend to use the customizable Wagner with equally spaced parallels (see below) instead, which also can render the Wagner III.
For informations about the parameters and their notation, and the reason to develop two different approaches for the Wagner III, read this blogpost.
➡ Example #1 | Example #2 | Interactive Demo -
The customizable Wagner ESP.
Lenticular projections with equally spaced parallels (along the central meridian). You can adjust the length of the pole line, the aspect ratio of the axes and the curvature of parallels, rendering all possible variants of Wagner III, VI and IX. Moreover, you can apply Wagner’s Umbeziffern to three other parent projections, thereby creating projections that, to my knowledge, have only been used in cartographic literature but not in practice – or not at all.
Most of the source code wasn’t written by me but by Peter Denner – thanks a lot! :-)
For informations about the parameters and their notation, read this blogpost.
➡ Example #1 | Example #2 | Example #3 | Example #4 | Interactive Demo -
The customizable Wagner VII/VIII.
In my opinion the most interesting variant. A lenticular projection, you can modify the length of the pole line, the curvature of parallels, the aspect ratio of the axes and the amount of areal inflation (incl. zero = equal area).
I originally introduced this in 2018, since v2.5.0 it’s available in d3-geo-projection. So for the moment, it’s the only variant that you can use when you download the current release.
➡ Example #1 | Example #2 | Example #3 | Example #4 | Interactive Demo -
… and what about the customizable Wagner IV and V?
Currently, they don’t exist and most likely I won’t be handing them in for the foreseeable future. You’ll have to settle for seven of nine Wagner projections.
Source code
The customizable Wagner VII/VIII is part of
current d3-geo-projection releases.
Full source code: View d3-geo-projection.umbeziffern.rfc.js
Here is an excerpt showing the new code only:
// wagner2 und wagner3 by Tobias Jung
///////////////////////////////////////////////////////////
function wagnerEquallySpacedParallelsRaw(poleline, parallels, ratio, xfactor, parentProjection) {
// sanitizing the input values
// poleline and parallels may approximate but never equal 0.
// making the min value even smaller (e.g. 0.0001 or 1e-10) will render bugs.
poleline = max(poleline, 0.001);
parallels = max(parallels, 0.001);
// poleline must be <= 90; parallels may approximate but never equal 180
poleline = min(poleline, 90);
parallels = min(parallels, 179.99);
// ratio is max. 100, min 1
ratio = min(ratio, 600);
ratio = max(ratio, 1);
var p = ratio / 100,
ca = xfactor / 100,
k = sqrt( (p * poleline) / parallels ),
cm = poleline/90,
cn = parallels/180,
cx = k / ( sqrt( cm * cn ) ),
cy = 1 / (k*sqrt( cm * cn ) );
// console.log("Wagner's notation");
// console.log("cm = " + cm);
// console.log("cn = " + cn);
// console.log("cx = " + cx);
// console.log("cy = " + cy);
// // console.log("k = " + k);
// // console.log("p = " + 2 * pow(k, 2) * cn/cm);
// console.log("ca = " + ca);
console.log('ca = ' + ca + ', cm = ' + cm +', cn = ' + cn +', cx = ' + cx +', cy = ' + cy);
// ready to call the acutal formula:
return wagnerEquallySpacedParallelsFormula(ca, cm, cn, cx, cy, parentProjection);
}
function wagnerEquallySpacedParallels() {
var poleline = 70,
parallels = 50,
ratio = 200,
xfactor = 100,
parentProjection = 9,
mutate = d3Geo.geoProjectionMutator(wagnerEquallySpacedParallelsRaw),
projection = mutate(poleline, parallels, ratio, xfactor, parentProjection);
projection.poleline = function(_) {
return arguments.length ? mutate(poleline = +_, parallels, ratio, xfactor, parentProjection) : poleline;
};
projection.parallels = function(_) {
return arguments.length ? mutate(poleline, parallels = +_, ratio, xfactor, parentProjection) : parallels;
};
projection.ratio = function(_) {
return arguments.length ? mutate(poleline, parallels, ratio = +_, xfactor, parentProjection) : ratio;
};
projection.xfactor = function(_) {
return arguments.length ? mutate(poleline, parallels, ratio, xfactor = +_, parentProjection) : xfactor;
};
projection.parentProjection = function(_) {
return arguments.length ? mutate(poleline, parallels, ratio, xfactor, parentProjection = +_) : parentProjection;
};
return projection
.scale(112.314);
}
function wagnerEquallySpacedParallelsFormula(ca, cm, cn, cx, cy, parentProjection) {
if (parentProjection == 3) {
var pp = d3Geo.geoSinusoidalRaw;
} else if (parentProjection == 6) {
var pp = d3Geo.geoApian2Raw;
} else if (parentProjection == 9) {
var pp = d3Geo.geoAzimuthalEquidistantRaw;
} else if (parentProjection == 10) {
var pp = d3Geo.geoVanDerGrinten4Raw;
} else if (parentProjection == 11) {
var pp = d3Geo.geoPolyconicRaw;
} else if (parentProjection == 12) {
var pp = d3Geo.geoNicolosiRaw;
} else if (parentProjection == 13) {
var pp = d3Geo.geoRectangularPolyconicRaw(0);
}
function forward(lambda, phi) {
var xy = pp(cn * lambda, cm * phi),
x = ca * cx * xy[0],
y = cy * xy[1];
return [x, y];
}
forward.invert = function(x, y) {
var lambda_phi = pp.invert(x / (ca * cx), y / cy),
lambda = (1 / cn) * lambda_phi[0],
phi = (1 / cm) * lambda_phi[1];
return [lambda, phi];
};
return forward;
}
// wagner ii
function wagner2Formula(cx, cy, m1, m2) {
function forward(lambda, phi) {
phi = asin(m1 * sin(m2 * phi));
var x = cx * lambda * cos(phi),
y = (cy * phi);
return [ x, y ];
}
forward.invert = function(x, y) {
y /= cy;
return [
x / (cx * cos(y)),
asin(sin(y) / m1) / m2
];
};
return forward;
}
function wagner2Raw(poleline, inflation, ratio) {
// 60 is always used as reference parallel
var phi1 = pi / 3;
// 0 < poleline < 1
poleline = max(poleline, epsilon);
poleline = min(poleline, 1 - epsilon);
ratio = 100/ratio;
ratio = max(ratio, 0.1);
// ratio isn't really limited to 3,
// but this is about where it stops to make any sense...
ratio = min(ratio, 3);
inflation = inflation/100 + 1;
// 1 <= inflation < 2
inflation = max(inflation, 0);
inflation = min(inflation, 2 - epsilon);
var m2 = (acos(inflation * cos(phi1))) / phi1,
m1 = sqrt(1 - pow(poleline, 2)) / sin(m2 * (pi / 2)),
n = (asin(sqrt(1-pow(poleline, 2)))) / (ratio * pi),
cx = n / sqrt(n * m1 * m2),
cy = cx / n;
return wagner2Formula(cx, cy, m1, m2);
}
function wagner2() {
// default values generate wagner2
var poleline = 0.5,
inflation = 20,
ratio = 200,
mutate = d3Geo.geoProjectionMutator(wagner2Raw),
projection = mutate(poleline, inflation, ratio);
projection.poleline = function(_) {
return arguments.length ? mutate(poleline = +_, inflation, ratio) : poleline;
};
projection.inflation = function(_) {
return arguments.length ? mutate(poleline, inflation = +_, ratio) : inflation;
};
projection.ratio = function(_) {
return arguments.length ? mutate(poleline, inflation, ratio = +_) : ratio;
};
return projection
.scale(150);
}
// wagner iii (alternative approach)
function wagner3(poleline, ratio, phi0) {
// default values render the original wagner iii
var poleline = 0.5,
ratio = 200,
phi0 = 0;
var mutate = d3Geo.geoProjectionMutator(wagner3Raw),
projection = mutate(poleline, ratio, phi0);
projection.poleline = function(_) {
return arguments.length ? mutate(poleline = +_, ratio, phi0) : poleline;
};
projection.ratio = function(_) {
return arguments.length ? mutate(poleline, ratio = +_, phi0) : ratio;
};
projection.phi0 = function(_) {
return arguments.length ? mutate(poleline, ratio, phi0 = +_) : phi0;
};
return projection
.scale(176.84);
}
function wagner3Raw(poleline, ratio, phi0) {
ratio = ratio/100;
var cm = (2/pi) * acos(poleline),
cn = (ratio*cm)/2,
cy = cn / sqrt(cm*cn),
phi0 = phi0 * radians,
cosPhi = cos(phi0),
ca = cos(phi0) / ( cy * cos(cm*phi0)),
cx = cm / sqrt(cm*cn);
return wagner3Formula(cx, cy, ca, cm);
}
function wagner3Formula(cx, cy, ca, cm) {
function forward(lambda, phi) {
var y = cx * phi,
x = ca * cy * lambda * cos(cm*phi);
return [x,y];
}
forward.invert = function(x, y) {
y /= cx;
return [
x / (cy * cos(cm*y) * ca),
y
];
}
return forward;
}
function apian2Raw(lambda, phi) {
return [lambda * sqrt(1 - pow(phi / halfPi, 2)), phi];
}
apian2Raw.invert = function(x, y) {
return [x / sqrt(1 - pow(y / halfPi, 2)), y];
};
function apian2() {
return d3Geo.geoProjection(apian2Raw)
.scale(112.314);
}
// NOTE: The following function renders Frank Canters' optimization of Wagner IX.
// Actually it is not needed, because you can render this projection with:
// d3.geoWagnerEquallySpacedParallels().parentProjection(9).poleline(67.131).parallels(86.562).ratio(206.7978).xfactor(80.7486)
// However, Canters used a different mathematical approach which is reproduced here.
// If you're interested in the mathematics behind map projections, you might enjoy seeing his approach,
// but I guess it's not necassary to add this to d3-geo-projections.
function cantersW09Raw(cm, cn, k1, k2) {
var k = k2,
ca = k1 / k2,
p = 1/(2 * k1 * k2) * cm/cn,
cx = k / ( sqrt( cm * cn ) ),
cy = 1 / (k*sqrt( cm * cn ) );
// console.log("Canters's notation");
// console.log("m = " + cm);
// console.log("n = " + cn);
// console.log("k1 = " + k1);
// console.log("k2 = " + k2);
// console.log("p = " + p);
// console.log("");
// console.log("Wagner's notation");
// console.log("m = " + cm);
// console.log("n = " + cn);
// console.log("Cx = " + cx);
// console.log("Cy = " + cy);
// console.log("k = " + k);
// console.log("p = " + 2 * pow(k, 2) * cn/cm);
// console.log("a = " + ca);
// ready to call the actual formula:
return wagnerEquallySpacedParallelsFormula(ca, cm, cn, cx, cy, 9);
}
function cantersW09() {
// default values generate the original Canters W09
var cm = 0.7459,
cn = 0.4809,
k1 = 1.0226,
k2 = 1.2664,
mutate = d3Geo.geoProjectionMutator(cantersW09Raw),
projection = mutate(cm, cn, k1, k2);
projection.cm = function(_) {
return arguments.length ? mutate(cm = +_, cn, k1, k2) : cm;
};
projection.cn = function(_) {
return arguments.length ? mutate(cm, cn = +_, k1, k2) : cn;
};
projection.k1 = function(_) {
return arguments.length ? mutate(cm, cn, k1 = +_, k2) : k1;
};
projection.k2 = function(_) {
return arguments.length ? mutate(cm, cn, k1, k2 = +_) : k2;
};