D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
linard-y
Full window
Github gist
Hexagonal tessallation on an icosahedral face
Gnomonic projection on faces of an icosahedron and hexagonal tessellation
<!DOCTYPE html> <meta charset="utf-8"> <style> body { background: #fcfcfa; width: 1200px; } .hex { stroke: #FF0000; stroke-width: .2px; fill: transparent; opacity: 1; } .hex:hover { fill: red; opacity: 0.5; } .hex.selected:hover { fill: green; opacity: 0.5; } .hex.selected { fill: green; opacity: 0.5; } h1 { color: rgb(51, 102, 0) font-size: 2.5em; margin: 0; } .author a, .meta a { color: #000; } .author, .meta { color: #666; font-style: italic; font-size: small; } .sea { stroke: none; fill: #ccf; } .outline { stroke: #000; stroke-width: 1px; fill: none; } .graticule { stroke: #000; stroke-width: .25px; stroke-opacity: .5; fill: none; } .land { stroke: #000; stroke-width: .5px; fill: #9f9; } .face { stroke: #FF0000; stroke-width: .5px; stroke-dasharray: 8,3; fill: transparent; opacity: 1; cursor: pointer; } .face:hover { stroke: #FF0000; stroke-width: .5px; stroke-dasharray: 8,3; fill: red; opacity: 0.5; cursor: pointer; } .face.selected:hover { fill: green; opacity: 0.5; } .face.selected { fill: green; opacity: 0.5; } #faceselector { position: absolute; top: 80px; left: 20px; } #HexagonSizeSelector { position: absolute; top: 380px; left: 20px; } #renderer { background: beige; position: absolute; top: 80px; left: 400px; } #hexjsonscreen { background: beige; position: absolute; top: 650px; left: 400px; } </style> <body> <h1>Gnomonic projection on faces of an icosahedron and associated hexagonal regular grid</h1> <p class="author">created with <a href="https://d3js.org/">D3.js</a></p> <div id="faceselector"> <label tyle="display: inline-block; width: 240px; text-align: right">Select a face of the icosahedron foldout : </label> <div id="foldout"> </div> </div> <div id="HexagonSizeSelector"> <label for="GridSize" style="display: inline-block; width: 240px; text-align: left">subdivision = </label> <input type="number" min="0" max="96" id="GridSize" value=0> <p> <label style="display: inline-block; width: 540px; text-align: left">-> Number of hexagon on the face = <span id="Hex-value"></span></label> <br> <label style="display: inline-block; width: 540px; text-align: left">-> Characteristic dimension of one hexagon</label> <ul> <li><span id="Arc-value"></span> rad</li> <li><span id="Size-value"></span> miles</li> <li><span id="Size2-value"></span> km</li> </ul> </p> </div> <div id="renderer"></div> <div> <textarea id="hexjsonscreen" rows="6" cols="87"> </textarea> </div> <script src="https://d3js.org/d3.v4.min.js"></script> <script src="https://d3js.org/topojson.v2.min.js"></script> <script src="d3-geo-projection.js"></script> <script src="d3-geo.js"></script> <script src="platonic_solids.js"></script> <script> var ε = 1e-2; var id_face = 3; var sub = 0; var sel_hex = '+0 +0' var width1 = 300; var height1 = width1/ d3.icosahedron.sizeratio; var margin1 = 1; var width2 = 12*width1; // sert à définir la taille du foldout complet d'où en découle la taille d'une face var height2 = width2/ d3.icosahedron.sizeratio; var margin2 = 1; var svg1 = d3.select("#foldout").append("svg"); var renderer = d3.select("#renderer"); var hexjsonscreen = d3.select("#hexjsonscreen"); var svg2; var svg3; var wface; var hface; var localHexagons; // Add an event listener to the #GridSize input created in the html part d3.select("#GridSize").on("input", changeSize ) // Define here how to project the globe on each face of an icosahedron // We choose a gnomonic projection centered on the face's centroid var faceProjection = function(face) { var c = d3.geoCentroid({type: "MultiPoint", coordinates: face}); if (Math.abs(Math.abs(c[1]) - 90) < 1e-6) c[0] = 0; return d3.geoGnomonic().scale(1).translate([0, 0]).rotate([-c[0], -c[1]]); }; var projection1 = d3.icosahedron.projection(faceProjection); projection1.fitExtent([ [margin1, margin1], [width1-margin1, (width1-margin1)/ d3.icosahedron.sizeratio] ], {type:"Sphere"}); projection1.rotate([162,0,0]); var projection2 = d3.icosahedron.projection(faceProjection); projection2.fitExtent([ [margin2, margin2], [width2-margin2, (width2-margin2)/ d3.icosahedron.sizeratio] ], {type:"Sphere"}); projection2.rotate([162,0,0]); var path1 = d3.geoPath().projection(projection1); var path2 = d3.geoPath().projection(projection2); var graticule = d3.geoGraticule(); var rot = d3.geoRotation([-162,0,0]); var geofaces = d3.icosahedron.faces.map(function(d) { var P0 = d3.geoInterpolate(d.centroid, d[0])(1-ε); var P1 = d3.geoInterpolate(d.centroid, d[1])(1-ε); var P2 = d3.geoInterpolate(d.centroid, d[2])(1-ε); var poly = {id: d3.icosahedron.faces.indexOf(d), type: "Polygon", coordinates: [[rot(P0), rot(P1), rot(P2), rot(P0)]], geocentroid:rot(d.centroid)}; return poly; }) svg1 .attr("width", width1) .attr("height", height1); var definitions1 = svg1.append("defs"); definitions1.append("clipPath") .attr("id", "sphereclip") .append("path") .datum({type: "Sphere"}) .attr("d", path1); svg1.append("path") .datum({type: "Sphere"}) .attr("class", "sea") .attr("d", path1); svg1.append("path") .datum(graticule) .attr("class", "graticule") .attr("d", path1); d3.json("world-110m.json", function(error, world) { var land = topojson.feature(world, world.objects.land); svg1 .insert("path", ".graticule") .datum(land) .attr("class", "land") .attr("clip-path", "url(#sphereclip)") .attr("d", path1); }); svg1.selectAll('face') .data(geofaces) .enter() .append('path') .attr("class", function(d){ if (d.id==id_face) { return "face selected"; } else { return "face"; } }) .attr("d", path1) .on("click", function(d) { svg1.selectAll(".selected").classed("selected", false); d3.select(this).classed("selected", true); id_face = d.id; renderer.selectAll("svg").remove(); renderface(); changeselection(); }); svg1.selectAll('faceindex') .data(geofaces) .enter() .append('text') .attr("class", "faceindex") .attr("x", function(d){return projection1(d.geocentroid)[0];}) .attr("y", function(d){return projection1(d.geocentroid)[1];}) .style("fill", "black") .style("font-size", '10px') .attr("dy", ".35em") .attr("text-anchor", "middle") .style("pointer-events", "none") .style("text-align", "left") .text(function(d){return d.id;}); svg1.append("path") .datum({type: "Sphere"}) .attr("class", "outline") .attr("d", path1); svg1.selectAll('.symbol') .data([projection1(projection1.center())]) .enter().append('path') .attr("class", "symbol") .attr('d', d3.symbol().type(d3.symbols[1]) ) .attr('transform',function(d){ return 'translate('+ d +')';}); renderface(); changeselection(); //////////////////////// functions //////////////////////////////////////////// function polygon(d) { return "M" + d.join("L") + "Z"; } function round(value, precision) { var multiplier = Math.pow(10, precision || 0); return Math.round(value * multiplier) / multiplier; } function dist(a,b){ var x = a[0] - b[0]; var y = a[1] - b[1]; return Math.sqrt( x*x + y*y ); } function translate(pts, V1, V2){ V=[V2[0]-V1[0],V2[1]-V1[1]] return pts.map(function(P){return [P[0]+V[0],P[1]+V[1]];}); } function pair(chiffre){ chiffre=parseInt(chiffre); return ((chiffre & 1)=='0')?true:false; } function sign(i){ return [0,1,2,3,4,6,8,10,12,14].indexOf(i)==-1 ? -1 : 1; } function subdivide(tri, n) { var side01 = interpolate(tri[0], tri[1]), side12 = interpolate(tri[1], tri[2]), side20 = interpolate(tri[2], tri[0]), axe0 = interpolate(tri[0],side12(1 / 2)), axe1 = interpolate(tri[1],side20(1 / 2)), axe2 = interpolate(tri[2],side01(1 / 2)); var hex = []; var r = dist(tri[0], tri[1])/(3*(n+1)); var sens = '+'; if (sign(id_face)==-1){sens='-';} for (var i = 0; i <= (2*n+1); ++i) { if (pair(n)){offset=0;} else {offset=1/(3*(n+1));} c = axe0(offset+((3*n + 2)/(3*(n+1)))-((2*i)/(3*(n+1)))) p0=[c[0]+r,c[1]] p1=[c[0]+r/2,c[1]-r*(Math.sqrt(3)/2)] p2=[c[0]-r/2,c[1]-r*(Math.sqrt(3)/2)] p3=[c[0]-r,c[1]] p4=[c[0]-r/2,c[1]+r*(Math.sqrt(3)/2)] p5=[c[0]+r/2,c[1]+r*(Math.sqrt(3)/2)] hex.push({'id':sens+i+' +0','type':'Polygon','coordinates':[p0,p1,p2,p3,p4,p5,p0]}); var cp=c,pp0=p0,pp1=p1,pp2=p2,pp3=p3,pp4=p4,pp5=p5; var cm=c,pm0=p0,pm1=p1,pm2=p2,pm3=p3,pm4=p4,pm5=p5; for (var j = 1; j <= (n+1); ++j) { if (pair(j)){ a = interpolate(pp0,pp1)(1/2); b = interpolate(cp,a)(2); d = interpolate(pm3,pm2)(1/2); e = interpolate(cm,d)(2); } else { a = interpolate(pp0,pp5)(1/2); b = interpolate(cp,a)(2); d = interpolate(pm3,pm4)(1/2); e = interpolate(cm,d)(2); } [cp,pp0,pp1,pp2,pp3,pp4,pp5] = translate([cp,pp0,pp1,pp2,pp3,pp4,pp5], cp, b); hex.push({'id':sens+i+' +'+j,'type':'Polygon','coordinates':[pp0,pp1,pp2,pp3,pp4,pp5,pp0]}); [cm,pm0,pm1,pm2,pm3,pm4,pm5] = translate([cm,pm0,pm1,pm2,pm3,pm4,pm5], cm, e); hex.push({'id':sens+i+' -'+j,'type':'Polygon','coordinates':[pm0,pm1,pm2,pm3,pm4,pm5,pm0]}); } } return hex; } function interpolate(p0, p1) { return function(t) { return [p0[0] + t * (p1[0] - p0[0]), p0[1] + t * (p1[1] - p0[1])]; };} function renderface(){ var geoface = geofaces.filter(function(d){ return d.id == id_face; })[0]; //var centroid = projection2(geoface.geocentroid); var side = dist(projection2(geoface.coordinates[0][0]), projection2(geoface.coordinates[0][1])) wface = margin2 + (Math.round(side)+1); hface = margin2 + (Math.round((Math.sqrt(3)*side/2))+1); var centre = [wface/2 - projection2(geoface.geocentroid)[0], hface/2 + sign(id_face)*(Math.sqrt(3)/12)*side - projection2(geoface.geocentroid)[1]]; // on définit les subdivisions sur la face (plane) de l'icosaèdre localHexagons = subdivide([projection2(geoface.coordinates[0][0]), projection2(geoface.coordinates[0][1]), projection2(geoface.coordinates[0][2])], sub); svg2 = renderer.append('svg').attr("id", "svg"); svg2 .attr("width", wface) .attr("height", hface); var definitions = svg2.append("defs"); definitions.append("clipPath") .attr("id", "faceclip"+id_face) .append("path") .datum(geoface) .attr("d", path2); var maplayer = svg2.append("g"); maplayer.append("path") .datum({type: "Sphere"}) .attr("class", "sea") .attr("id", "sphere"+id_face) .attr("d", path2) .attr("clip-path", "url(#faceclip"+id_face+")") .attr("transform", "translate(" + centre[0] + "," + centre[1] + ")"); maplayer.append("path") .datum(graticule) .attr("class", "graticule") .attr("clip-path", "url(#faceclip"+id_face+")") .attr("transform", "translate(" + centre[0] + "," + centre[1] + ")") .attr("d", path2); maplayer.append("path") .datum(geoface) .attr("class", "face") .attr("transform", "translate(" + centre[0] + "," + centre[1] + ")") .attr("d", path2); d3.json("world-110m.json", function(error, world) { maplayer.insert("path", ".graticule") .datum(topojson.feature(world, world.objects.land)) .attr("class", "land") .attr("clip-path", "url(#faceclip"+id_face+")") .attr("transform", "translate(" + centre[0] + "," + centre[1] + ")") .attr("d", path2); }); var hexgridlayer = svg2.append("g") .attr("id", "hex"); hexgridlayer.selectAll("hex") .data(localHexagons) .enter() .append("path") .attr("class", function(d){ if (d.id==sel_hex) { return "hex selected"; } else { return "hex"; } }) .attr("clip-path", "url(#faceclip"+id_face+")") .attr("d", function(d){return polygon(d['coordinates']);}) .attr("transform", "translate(" + centre[0] + "," + centre[1] + ")") .on("click", function(d) { svg2.selectAll(".selected").classed("selected", false); d3.select(this).classed("selected", true); sel_hex = d.id; changeselection(); }); if (sub<=24) { var indexlayer = svg2.append("g") .attr("id", "index"); indexlayer.selectAll("index") .data(localHexagons) .enter() .append("text") .attr("class", "index") .attr("clip-path", "url(#faceclip"+id_face+")") .attr("x", function(d){return d3.polygonCentroid(d['coordinates'])[0];}) .attr("y", function(d){return d3.polygonCentroid(d['coordinates'])[1];}) .style("fill", "black") .style("font-size", function(d){ if (sub<=6){return '10px';} else if (sub<=12){return '8px';} else if (sub<=18){return '6px';} else {return '4px';}; }) .attr("dy", ".35em") .attr("text-anchor", "middle") .style("pointer-events", "none") .style("text-align", "left") .text(function(d){return d['id'];}) .attr("transform", "translate(" + centre[0] + "," + centre[1] + ")"); } d3.select("#Hex-value").text((1.5*sub**2+4.5*sub+4) + ' ( including ' + (1.5*sub**2+1.5*sub+1) + ' complete)'); d3.select("#Arc-value").text(2*d3.geoDistance(projection2.invert(localHexagons[0].coordinates[0]),projection2.invert(localHexagons[0].coordinates[1]))); d3.select("#Size-value").text(Math.round(2*3064*d3.geoDistance(projection2.invert(localHexagons[0].coordinates[0]),projection2.invert(localHexagons[0].coordinates[1])))); d3.select("#Size2-value").text(Math.round(1.60934*2*3064*d3.geoDistance(projection2.invert(localHexagons[0].coordinates[0]),projection2.invert(localHexagons[0].coordinates[1])))); } function changeSize() { sub=parseInt(this.value); renderer.selectAll("svg").remove(); renderface(); changeselection(); } function changeselection() { document.getElementById("hexjsonscreen").value = get_geojson(); //hexjsonscreen // .datum(get_geojson()) // .property('value', d); } function get_geojson() { var sel = localHexagons.filter(function(d){ return d.id == sel_hex; })[0]; var obj_hex = { "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": {"id": sel.id}, "geometry": { "type": "Polygon", "coordinates": (sel.coordinates.map(projection2.invert)).map(d3.geoRotation([360,0,0])) } } ] } return JSON.stringify(obj_hex) } // Mystara // R = 3095 miles = 4980,92 km //Earth // R = 6378 miles = 10264,4 km // 1 miles = 1,60934 km </script> </body> </html>
https://d3js.org/d3.v4.min.js
https://d3js.org/topojson.v2.min.js