-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
195 lines (185 loc) · 7.62 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- <meta name="viewport" content="width=device-width"> -->
<script src="https://d3js.org/d3.v5.js" charset="utf-8"></script>
<title>D3 Map Component</title>
<style>
body {
margin: 0;
padding: 0;
border: 0;
font-family: Arial, Helvetica, sans-serif;
}
svg#visualization {
position: absolute;
height: 100%;
width: 100%;
}
.overlay {
position: absolute;
background-color:lightblue;
padding: 5px;
border-radius: 5px;
/* outline: medium solid grey; */
}
details { top: 5%; left: 5%; right: 5% }
.coordinates { bottom: 5%; right: 5%; }
.info { bottom: 5%; left: 5%; }
g.territory, use.target {
stroke: grey;
stroke-width: 3px;
fill: none;
}
.h4000 { fill: #FFFFFF; }
.h3500 { fill: #F8F8F8; }
.h3000 { fill: #EEEEEE; }
.h2500 { fill: #E8E8E8; }
.h2000 { fill: #DDDDDD; }
.h1500 { fill: #D8D8D8; }
.h1000 { fill: #CCCCCC; }
.h500 { fill: #C8C8C8; }
.h0 { fill: #BBBBBB; }
</style>
</head>
<body>
<svg id="visualization"
viewBox="0 0 500 600"
preserveAspectRatio="xMidYMid meet"
pointer-events="all">
<g class="atlas">
</g>
<defs>
<g id="sink">
<title> Sink </title>
<desc> A glyph representing a sink. </desc>
<path d="M10,0 H-10 M0,10 V-10"></path>
</g>
</defs>
</svg>
<h4 class="overlay coordinates"></h3>
<h4 class="overlay info"></h3>
<details class="overlay">
<summary>Composite D3 Components</summary>
<p> A map of Nevada's terrain and census tracts. The tracts come from the 2017 census, and
the contours were computed from The Shuttle Radar Topography Mission data using D3.
</p>
<p> This visualization is a composite of D3 components. An 'Atlas' component holds a geographic
projection, a camera transform, and a stack of components which are rendered in order. It
is a D3 style component which applies the General Update Pattern recursively to it's
constituent components.
</p>
<p> The 'Contour' component creates projected SVG paths. It is added twice to the Atlas; Once
to draw the territories, and once to draw the elevation contours. The only difference
between them is the CSS styling for fill and stroke. They are loaded asynchronously and
the map is refreshed as they arrive.
</p>
<p> A 'Stamp' component clones SVG definitions. In this case, I put a '+'-shaped marker at every
corner of the original SRTM raster tiles. Notice how the markers do not change size as you
zoom in and out on the terrain? Layers can be marked so the Atlas does not apply its
transforms to them, which is useful for overlays.
</p>
<p> Mouse interactivity can be added to any component. In this example mouse movement calculates
the geodetic coordinates of the pointer using the inverse of the Atlas's transforms.
Carefully clicking on a Stamp marker will printout the the data contents of that marker in
the bottom left corner of the screen. Dragging and scrolling updates the camera transform
using D3.Zoom.
</p>
<p> Compositing components and then attaching behavior to them in this way makes extensibility
and code reuse much easier. Though I'm not sure I got the container interface polished
enough, I still think I got the right idea for more complex dashboards or subtly different
customer needs.</p>
</details>
<script type="module">
import Atlas from './container/atlas.js';
import Contour from './component/contour.js';
import Stamp from './component/stamp.js';
import Navigation from './navigation.js';
main();
async function main() {
let svg = d3.select('svg#visualization > g.atlas');
let coordinates = d3.select('.coordinates');
let info = d3.select('.info');
// Mark each intersection of integer longitude and latitude
let targets = [];
for (let x=-120; x<=-114; x++)
for (let y=35; y<=42; y++)
targets.push( {lon:x, lat:y, class:'target', glyph:'sink'} );
// lets me visually check marks and contours have the same projection
// a list of GeoJson Files to load elevation countours from in ascending order
let files = [
`data/N35W120H0.json`,
`data/N35W120H500.json`,
`data/N35W120H1000.json`,
`data/N35W120H1500.json`,
`data/N35W120H2000.json`,
`data/N35W120H2500.json`,
`data/N35W120H3000.json`,
`data/N35W120H3500.json`,
`data/N35W120H4000.json`
];
// The initial state. We'll asynchronously add data to it as large files load
let state = [
{classy:'targets', data:targets},
{classy:'territory', data:[]},
{classy:'terrain', data:[{type:'Sphere', value:0}]}
];
// Construct the map by compositing components
let atlas = Atlas(svg, {})
.move( function() {
let screen = d3.mouse(this);
let sphere = atlas.screen2sphere(screen);
let lon = Navigation.degrees2sexagesimal( sphere[0], 'W', 'E' );
let lat = Navigation.degrees2sexagesimal( sphere[1], 'S', 'N' );
coordinates.html(`${lat} / ${lon}`);
});
atlas
.addLayer( 'terrain', Contour()
.projection( atlas.projection() )
.classifier( d=> (d==undefined || d==null) ? null : `h${d.value}` ) )
.addLayer( 'territory', Contour()
.projection( atlas.projection() )
.classifier( d=>(d==undefined) ? null : `tract${d.properties.TRACTCE}`) )
.addLayer( 'targets', Stamp()
.position( function(d) {return atlas.sphere2screen([d.lon, d.lat]);} )
.classifier( d=>d.class )
.glyph( d=>d.glyph )
.clicker( showInfo ) );
// the default click action updates the info panel
function showInfo(d) {
if(!d || d==undefined)
return;
let json = JSON.stringify(d, null, '\t');
info.html(`<pre>${json}</pre>`);
}
// initial render
atlas(svg, state);
// load the GeoJson census tract features asynchronously
d3.json('data/2017tractsNV.json')
.then( function(tracts) {
state[1].data = tracts.features;
atlas(svg, state);
} );
// load the elevation contours asyncronously, but one after the other
loadNext( state[2].data );
function loadNext(contours) {
if (contours.length == files.length+1)
return;
d3.json( files[contours.length-1] )
.then( function(elevation) {
contours.push( elevation );
atlas(svg, state);
loadNext( contours )
} );
}
}
// // default color scale for elevation, cribbed from 'https://en.wikipedia.org/wiki/Wikipedia:WikiProject_Maps/Conventions', though there are no given corresponding heights...
// let color = d3.scaleQuantize()
// .range(['#ACD0A5','#94BF8B','#A8C68F','#BDCC96','#D1D7AB','#E1E4B5',//green to tan
// '#EFEBC0','#E8E1B6','#DED6A3','#D3CA9D','#CAB982','#C3A76B','#B9985A','#AA8753',//tan to brown
// '#AC9A7C','#BAAE9A','#CAC3B8','#E0DED8','#F5F4F2'])// brown to white
// .domain([-100, 4400]);
</script>
</body>
</html>