maxxxzdn commited on
Commit
7b1476a
·
verified ·
1 Parent(s): 8f493c4

UI polish: script.js

Browse files
Files changed (1) hide show
  1. script.js +50 -29
script.js CHANGED
@@ -13,20 +13,20 @@ const PLOTLY_CMAP = {
13
  magma:['Magma',false], YlGnBu:['YlGnBu',false], RdBu:['RdBu',false],
14
  RdBu_r:['RdBu',true],
15
  };
16
- const R = 1.0; // sphere radius
17
- const COAST_R = 1.004; // coastline shell, just above the surface
18
- const LON0 = -90; // deg offset so lon 0° faces the default camera
19
  const DEG = Math.PI / 180;
20
 
21
  const app = {
22
  index:null, coast:null, meta:null,
23
- truthF32:null, // base file: denorm ground truth (T·nLon·nLat)
24
- meanCache:{}, curMean:null, // n -> denorm mean field; active one
25
- loadingN:0, // ensemble size currently being fetched
26
- meanFrames:null, rightFrames:null, // per-t 2D surfacecolor (nLat × W)
27
- geom:null, coastXYZ:null, // shared sphere mesh + coastline lines
28
  M:0, T:0, nLon:0, nLat:0, W:0,
29
  vmean:0, vstd:1,
 
30
  rightMode:'truth', n:16, t:0, dragging:false,
31
  curHash:null, curCache:null,
32
  currentIC:null, currentVar:null, playing:false, timer:null, autoplay:true,
@@ -43,12 +43,26 @@ function h2f(h) {
43
  const _MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
44
  function fmtIC(s){ const [y,m] = s.split('-'); return `${_MON[+m-1]} ${y}`; } // Feb 2016
45
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  async function init() {
47
- const [index, coast] = await Promise.all([
48
  fetch(INDEX_URL).then(r => r.json()),
49
  fetch(COAST_URL).then(r => r.json()),
 
50
  ]);
51
  app.index = index; app.coast = coast;
 
52
 
53
  buildTokens('ic-tokens',
54
  app.index.ics.map(ic => ({ label: fmtIC(ic.init_time), val: ic.hash })),
@@ -84,13 +98,28 @@ function markActive(id, val) {
84
  t.classList.toggle('active', t.dataset.val === val));
85
  }
86
 
87
- // One unit-sphere mesh (nLat × W, W = nLon+1 wrap column) shared by both
88
- // scenes, and the coastline polylines as sphere xyz. Rotation is the 3D
89
- // camera (synced across the two scenes), so geometry is static.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  function buildGeometry() {
91
  const lon = app.index.lon, lat = app.index.lat;
92
  const nLon = lon.length, nLat = lat.length, W = nLon + 1;
93
  app.nLon = nLon; app.nLat = nLat; app.W = W;
 
94
 
95
  const mk = () => Array.from({ length: nLat }, () => new Float32Array(W));
96
  const x = mk(), y = mk(), z = mk();
@@ -98,7 +127,7 @@ function buildGeometry() {
98
  const clat = Math.cos(lat[j] * DEG), slat = Math.sin(lat[j] * DEG);
99
  for (let i = 0; i < W; i++) {
100
  const lonDeg = (i < nLon ? lon[i] : lon[0] + 360);
101
- const lr = (lonDeg + LON0) * DEG;
102
  x[j][i] = R * clat * Math.cos(lr);
103
  y[j][i] = R * clat * Math.sin(lr);
104
  z[j][i] = R * slat;
@@ -108,8 +137,8 @@ function buildGeometry() {
108
 
109
  const X = [], Y = [], Z = [];
110
  for (const poly of app.coast) {
111
- for (const [lo, la] of poly) {
112
- const lr = (lo + LON0) * DEG, clat = Math.cos(la * DEG);
113
  X.push(COAST_R * clat * Math.cos(lr));
114
  Y.push(COAST_R * clat * Math.sin(lr));
115
  Z.push(COAST_R * Math.sin(la * DEG));
@@ -249,18 +278,14 @@ function sceneCfg(domainX) {
249
  };
250
  }
251
 
252
- // Fixed (paper-anchored) titles — NOT scene annotations, so they don't rotate
253
- // with the globe.
254
- function titleAnn(x, text) {
255
- return { x, y:1.0, xref:'paper', yref:'paper', text, showarrow:false,
256
- xanchor:'center', yanchor:'top',
257
- font:{ family:'IBM Plex Mono, monospace', size:16, color:'#aab0bb' } };
258
- }
259
-
260
  function drawFigure() {
261
  const m = app.meta, g = app.geom, c = app.coastXYZ, { t } = app;
262
  const [cmap, rev] = PLOTLY_CMAP[m.cmap] || ['Viridis', false];
263
  const diff = app.rightMode === 'diff';
 
 
 
 
264
  const light = { ambient:0.92, diffuse:0.32, specular:0.04, roughness:0.9, fresnel:0.0 };
265
  const lightpos = { x:-5000, y:-8000, z:12000 };
266
  const surf = (scene, color, scl, reverse, cmin, cmax) => ({
@@ -270,7 +295,7 @@ function drawFigure() {
270
  });
271
  const coast = (scene) => ({
272
  type:'scatter3d', mode:'lines', scene, x:c.x, y:c.y, z:c.z,
273
- line:{ color:'#aeb4bf', width:1.4 }, opacity:0.9,
274
  hoverinfo:'skip', showlegend:false,
275
  });
276
 
@@ -283,15 +308,11 @@ function drawFigure() {
283
  coast('scene2'),
284
  ];
285
  const layout = {
286
- autosize:true, margin:{ l:0, r:0, t:8, b:0 },
287
  paper_bgcolor:'rgba(0,0,0,0)', font:{ family:'Inter, sans-serif' },
288
  showlegend:false,
289
  scene: sceneCfg([0.055, 0.43]),
290
  scene2: sceneCfg([0.57, 0.945]),
291
- annotations:[
292
- titleAnn(0.2425, 'MOSAIC — ENSEMBLE MEAN'),
293
- titleAnn(0.7575, diff ? 'ERROR (MEAN − TRUTH)' : 'ERA5 — GROUND TRUTH'),
294
- ],
295
  };
296
 
297
  Plotly.newPlot('plot', data, layout,
 
13
  magma:['Magma',false], YlGnBu:['YlGnBu',false], RdBu:['RdBu',false],
14
  RdBu_r:['RdBu',true],
15
  };
16
+ const R = 1.0;
17
+ const COAST_R = 1.010; // coastline shell above the surface (higher = fewer occlusion gaps)
 
18
  const DEG = Math.PI / 180;
19
 
20
  const app = {
21
  index:null, coast:null, meta:null,
22
+ truthF32:null,
23
+ meanCache:{}, curMean:null,
24
+ loadingN:0,
25
+ meanFrames:null, rightFrames:null,
26
+ geom:null, coastXYZ:null,
27
  M:0, T:0, nLon:0, nLat:0, W:0,
28
  vmean:0, vstd:1,
29
+ LON0: -100, // default: ~10°E (Europe); overridden by geolocation
30
  rightMode:'truth', n:16, t:0, dragging:false,
31
  curHash:null, curCache:null,
32
  currentIC:null, currentVar:null, playing:false, timer:null, autoplay:true,
 
43
  const _MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
44
  function fmtIC(s){ const [y,m] = s.split('-'); return `${_MON[+m-1]} ${y}`; } // Feb 2016
45
 
46
+ // Try to get user longitude for globe centering; falls back to 10°E within 500 ms.
47
+ function getInitialLon() {
48
+ return new Promise(resolve => {
49
+ if (!navigator.geolocation) { resolve(10); return; }
50
+ navigator.geolocation.getCurrentPosition(
51
+ pos => resolve(pos.coords.longitude),
52
+ () => resolve(10),
53
+ { timeout: 500, maximumAge: Infinity }
54
+ );
55
+ });
56
+ }
57
+
58
  async function init() {
59
+ const [index, coast, userLon] = await Promise.all([
60
  fetch(INDEX_URL).then(r => r.json()),
61
  fetch(COAST_URL).then(r => r.json()),
62
+ getInitialLon(),
63
  ]);
64
  app.index = index; app.coast = coast;
65
+ app.LON0 = -90 - userLon;
66
 
67
  buildTokens('ic-tokens',
68
  app.index.ics.map(ic => ({ label: fmtIC(ic.init_time), val: ic.hash })),
 
98
  t.classList.toggle('active', t.dataset.val === val));
99
  }
100
 
101
+ // Subdivide polyline segments longer than maxDeg° to keep coastlines on the
102
+ // sphere surface (long chords dip below the surface and get occluded).
103
+ function interpPoly(poly, maxDeg = 3) {
104
+ const out = [];
105
+ for (let i = 0; i < poly.length; i++) {
106
+ out.push(poly[i]);
107
+ if (i < poly.length - 1) {
108
+ const [lo0, la0] = poly[i], [lo1, la1] = poly[i + 1];
109
+ const d = Math.sqrt((lo1-lo0)**2 + (la1-la0)**2);
110
+ const n = Math.ceil(d / maxDeg);
111
+ for (let k = 1; k < n; k++)
112
+ out.push([lo0 + (lo1-lo0)*k/n, la0 + (la1-la0)*k/n]);
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+
118
  function buildGeometry() {
119
  const lon = app.index.lon, lat = app.index.lat;
120
  const nLon = lon.length, nLat = lat.length, W = nLon + 1;
121
  app.nLon = nLon; app.nLat = nLat; app.W = W;
122
+ const L0 = app.LON0;
123
 
124
  const mk = () => Array.from({ length: nLat }, () => new Float32Array(W));
125
  const x = mk(), y = mk(), z = mk();
 
127
  const clat = Math.cos(lat[j] * DEG), slat = Math.sin(lat[j] * DEG);
128
  for (let i = 0; i < W; i++) {
129
  const lonDeg = (i < nLon ? lon[i] : lon[0] + 360);
130
+ const lr = (lonDeg + L0) * DEG;
131
  x[j][i] = R * clat * Math.cos(lr);
132
  y[j][i] = R * clat * Math.sin(lr);
133
  z[j][i] = R * slat;
 
137
 
138
  const X = [], Y = [], Z = [];
139
  for (const poly of app.coast) {
140
+ for (const [lo, la] of interpPoly(poly, 3)) {
141
+ const lr = (lo + L0) * DEG, clat = Math.cos(la * DEG);
142
  X.push(COAST_R * clat * Math.cos(lr));
143
  Y.push(COAST_R * clat * Math.sin(lr));
144
  Z.push(COAST_R * Math.sin(la * DEG));
 
278
  };
279
  }
280
 
 
 
 
 
 
 
 
 
281
  function drawFigure() {
282
  const m = app.meta, g = app.geom, c = app.coastXYZ, { t } = app;
283
  const [cmap, rev] = PLOTLY_CMAP[m.cmap] || ['Viridis', false];
284
  const diff = app.rightMode === 'diff';
285
+
286
+ const rl = document.getElementById('right-label');
287
+ if (rl) rl.textContent = diff ? 'ERROR (MEAN − TRUTH)' : 'ERA5 — GROUND TRUTH';
288
+
289
  const light = { ambient:0.92, diffuse:0.32, specular:0.04, roughness:0.9, fresnel:0.0 };
290
  const lightpos = { x:-5000, y:-8000, z:12000 };
291
  const surf = (scene, color, scl, reverse, cmin, cmax) => ({
 
295
  });
296
  const coast = (scene) => ({
297
  type:'scatter3d', mode:'lines', scene, x:c.x, y:c.y, z:c.z,
298
+ line:{ color:'#c8cdd6', width:2.5 }, opacity:1,
299
  hoverinfo:'skip', showlegend:false,
300
  });
301
 
 
308
  coast('scene2'),
309
  ];
310
  const layout = {
311
+ autosize:true, margin:{ l:0, r:0, t:0, b:0 },
312
  paper_bgcolor:'rgba(0,0,0,0)', font:{ family:'Inter, sans-serif' },
313
  showlegend:false,
314
  scene: sceneCfg([0.055, 0.43]),
315
  scene2: sceneCfg([0.57, 0.945]),
 
 
 
 
316
  };
317
 
318
  Plotly.newPlot('plot', data, layout,