Published
Edited
Aug 7, 2022
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
face_refresh = (time) => {
clearface(clock);
backface(clock);
ticks(clock, time);
numerals(clock, time);
center(clock);
hand(clock, time);
sun(clock, time);
return (time.getSeconds() % 2 == 0) ? 'tick' : 'tock'
}
Insert cell
clearface = (target) => {
const c2d = target.getContext('2d');
c2d.clearRect(0,0, target.width, target.height);
}
Insert cell
backface = (target) => {
const c2d = target.getContext('2d');
const lw = 2;
const dia = target.width - (lw * 2);
const rad = dia / 2 * .8;
const ctr = lw + (dia / 2);
const [pi,ccw,cw] = [Math.PI,true,false];
const colors = sky_gradient(sky_layout, Math.round(1.04*width));
const n = colors.length;
const inc = pi*2 / n;
let a;
for (let i = 0; i < n; i++) {
a = inc*i + (-3*pi/2);
wedge(target, ctr,ctr, a,a+inc, rad*.7, rad*.3, colors[i], 1,colors[i]);
}
}
Insert cell
ticks = (target, time) => {
const c2d = target.getContext('2d');
const ctr = target.width / 2;
const s = start_of_day(time).getTime();
const ms_day = 1000*60*60*24;
const ticks_hr = 6;
const j = hrs.length * ticks_hr;
const ms_tick = ms_day / j;
const [n0,n1,n2] = [ctr*.80,ctr*.05,ctr*.03];
let p1, p2, is_hr;
c2d.strokeStyle = clock_palette.ink;
for (let i = 0; i < j; i++) {
is_hr = (i % ticks_hr == 0);
p1 = time_to_point(i*ms_tick + s, n0);
p2 = time_to_point(i*ms_tick + s, n0 + (is_hr ? n1 : n2));
c2d.beginPath();
c2d.lineWidth = is_hr ? 1.5 : 1;
c2d.moveTo(ctr+p1.x, ctr+p1.y);
c2d.lineTo(ctr+p2.x, ctr+p2.y);
c2d.stroke();
}
}
Insert cell
numerals = (target, time) => {
const c2d = target.getContext('2d');
const ctr = target.width / 2;
const s = start_of_day(time).getTime();
const fs = Math.max(8, target.width / 40);
c2d.font = `bold ${fs}px serif`;
c2d.textAlign = 'center';
c2d.fillStyle = clock_palette.ink;
hrs.forEach((v,i) => {
const p = time_to_point(i*60*60*1000 + s, ctr*.92);
c2d.fillText(v, ctr+p.x, ctr+p.y+(fs/2.75));
});
}
Insert cell
center = (target) => {
const c2d = target.getContext('2d');
const ctr = target.width / 2;
const r = target.width / 3.55;

c2d.strokeStyle = clock_palette.ink;
c2d.lineWidth = 1;

if (has_solar_crossing(sun_data_local)) {
const set = time_to_radians(sun_data_local.sunset.getTime());
const rise = time_to_radians(sun_data_local.sunrise.getTime());
c2d.fillStyle = clock_palette.center_light;
c2d.beginPath();
c2d.arc(ctr,ctr, r, rise, set);
c2d.fill();
c2d.stroke();
c2d.fillStyle = clock_palette.center_dark;
c2d.beginPath();
c2d.arc(ctr,ctr, r, set, rise);
c2d.fill();
c2d.stroke();
}
else {
c2d.fillStyle = is_polar_night(sun_data_local) ? clock_palette.center_dark : clock_palette.center_light;
c2d.beginPath();
c2d.arc(ctr,ctr, r, 2*Math.PI, 0);
c2d.fill();
c2d.stroke();
}
}
Insert cell
hand = (target, time) => {
const c2d = target.getContext('2d');
const ctr = target.width / 2;
const p = time_to_point(time.getTime(), ctr*.85);
c2d.strokeStyle = clock_palette.hand;
c2d.lineWidth = 3;
c2d.beginPath();
c2d.moveTo(ctr, ctr);
c2d.lineTo(ctr+p.x, ctr+p.y);
c2d.stroke();
c2d.fillStyle = clock_palette.ink;
c2d.lineWidth = 4.5;
c2d.beginPath();
c2d.arc(ctr,ctr, ctr*.01, 0,2*Math.PI);
c2d.stroke();
c2d.fill();
}
Insert cell
sun = (target, time) => {
const c2d = target.getContext('2d');
const ctr = target.width / 2;
const p = time_to_point(time.getTime(), ctr*.68);
c2d.strokeStyle = clock_palette.hand;
c2d.fillStyle = clock_palette.sun;
c2d.lineWidth = 4.5;
c2d.beginPath();
c2d.arc(ctr+p.x, ctr+p.y, ctr*.10, 0,2*Math.PI);
c2d.stroke();
c2d.fill();
}
Insert cell
Insert cell
Insert cell
Insert cell
clock_palette = ({
'center_dark': '#080363',
'center_light': '#b2d0ef',
'hand': '#f0d400',
'sun': '#fff',
'ink': '#000',
})
Insert cell
Insert cell
sky_palette = ({
'astronomical_twilight_begin': '#05000e',
'nautical_twilight_begin': '#3b1604',
'civil_twilight_begin': '#e9a63e',
'sunrise': '#eccc5b',
'am': '#fffef8',
'solar_noon': '#2499e0',
'pm': '#fafeff',
'sunset': '#ffb5de',
'civil_twilight_end': '#f9868b',
'nautical_twilight_end': '#6b6aa3',
'astronomical_twilight_end': '#050061',
})
Insert cell
Insert cell
sky_layout = {
let p = {};
let d = {};
Object.keys(sun_data_local).forEach((k) => {
if (valid_time(sun_data_local[k])) {
p[k] = sky_palette[k];
d[k] = sun_data_local[k];
}
});
if (Object.entries(p).length == 0) {
p = { empty: '#6b6aa3' };
d = { empty: '1970-01-01T00:00:01+00:00' };
}
const scale = d3.scaleLinear()
.domain([-3*Math.PI/2, Math.PI/2])
.range([0, 1]);
return Object.keys(p).map((k) => {
const a = time_to_radians(d[k]);
return { color: p[k], scale: scale(a), angle: a, name: k };
}).sort((a,b) => (a.scale < b.scale) ? -1 : ((a.scale > b.scale) ? 1 : 0));
}
Insert cell
sun_data_local = {
if (sun_data_utc.status !== 'OK') return {};
const utc = sun_data_utc.results;
const local = {};
const ignoring = ['day_length'];
if (is_polar_night(utc)) {
ignoring.push('sunrise','solar_noon','sunset');
}
else if (is_polar_day(utc)) {
ignoring.push('sunrise','sunset');
ignoring.push('civil_twilight_begin','civil_twilight_end');
ignoring.push('nautical_twilight_begin','nautical_twilight_end');
ignoring.push('astronomical_twilight_begin','astronomical_twilight_end');
const noon = new Date(utc['solar_noon']).getTime();
const hrs8 = 1000 * 60 * 60 * 8;
local['am'] = new Date(noon - hrs8);
local['pm'] = new Date(noon + hrs8);
}
else { // has_solar_crossing
local['am'] = time_lerp(new Date(utc['civil_twilight_begin']), new Date(utc['solar_noon']), .2);
local['pm'] = time_lerp(new Date(utc['solar_noon']), new Date(utc['civil_twilight_end']), .8);
}
Object.keys(utc).forEach(k => {
if (ignoring.includes(k)) { return; }
local[k] = new Date(utc[k]);
});
return local;
}
Insert cell
sun_data_utc = d3.json(sun_api_request)
Insert cell
sun_api_request = `${sun_api}?lat=${loc.latitude}&lng=${loc.longitude}&date=today&formatted=0`
Insert cell
loc = !geolocation_requested ? (selected_location && selected_location[1]) ?? default_location : new Promise((resolve, reject) => {
if (navigator.geolocation) navigator.geolocation.getCurrentPosition(
pos => resolve({label:'current position', latitude:pos.coords.latitude, longitude:pos.coords.longitude}),
err => resolve(default_location),
);
else resolve(default_location)
});
Insert cell
geolocation_requested = (selected_location && selected_location[1].label == 'geolocation requested')
Insert cell
geolocation_allowed = ['granted', 'prompt'].includes(geolocation_permission)
Insert cell
geolocation_permission = {
const query = await navigator?.permissions?.query({name: 'geolocation'});
return query?.state ?? 'denied'; // ['denied', 'prompt', 'granted']
}
Insert cell
default_location = world.san_francisco
Insert cell
Insert cell
Insert cell
sky_gradient = (palette, n=100) => { // palette = [{color:<#hexstring>,scale:<float>}]
let a = [];
let z = palette[palette.length-1];
let ca = z.color, cb;
let sa = 0, sb;
palette.forEach((v) => {
cb = v.color;
sb = v.scale;
a = a.concat(d3.quantize(d3.interpolateRgb.gamma(2.2)(ca, cb), Math.max(4,Math.round((sb-sa)*n))));
ca = cb;
sa = sb;
});
cb = z.color;
sb = 1;
a = a.concat(d3.quantize(d3.interpolateRgb.gamma(2.2)(ca, cb), Math.max(4,Math.round((sb-sa)*n))));
return a;
}
Insert cell
wedge = (target, cx,cy, a1,a2, d,t, fill='black',line=0,stroke='black') => {
const c2d = target.getContext('2d');
if (line > 0) {
c2d.strokeStyle = stroke;
c2d.lineWidth = line;
}
c2d.fillStyle = fill;
c2d.beginPath();
c2d.arc(cx,cy, d, a1,a2);
c2d.lineTo(cx+(Math.cos(a2)*(d+t)), cy+(Math.sin(a2)*(d+t)));
c2d.arc(cx,cy, d+t, a2,a1, true);
c2d.closePath();
c2d.fill();
if (line > 0) c2d.stroke();
}
Insert cell
has_solar_crossing = (sun_data) => (valid_time(new Date(sun_data['sunrise'])) && valid_time(new Date(sun_data['sunset'])))
Insert cell
is_polar_night = (sun_data) => {
if (has_solar_crossing(sun_data)) { return false; }
if (
valid_time(new Date(sun_data['civil_twilight_begin']))
|| valid_time(new Date(sun_data['nautical_twilight_begin']))
|| valid_time(new Date(sun_data['astronomical_twilight_begin']))
|| valid_time(new Date(sun_data['astronomical_twilight_end']))
|| valid_time(new Date(sun_data['nautical_twilight_end']))
|| valid_time(new Date(sun_data['civil_twilight_end']))
) { return true; }
return false;
}
Insert cell
is_polar_day = (sun_data) => {
if (has_solar_crossing(sun_data)) { return false; }
if (
valid_time(new Date(sun_data['civil_twilight_begin']))
|| valid_time(new Date(sun_data['nautical_twilight_begin']))
|| valid_time(new Date(sun_data['astronomical_twilight_begin']))
|| valid_time(new Date(sun_data['astronomical_twilight_end']))
|| valid_time(new Date(sun_data['nautical_twilight_end']))
|| valid_time(new Date(sun_data['civil_twilight_end']))
) { return false; }
return true;
}
Insert cell
time_to_point = (ms, r=1) => radians_to_point(time_to_radians(ms), r)
Insert cell
radians_to_point = (r, n=1) => ({ x: Math.cos(r)*n, y: Math.sin(r)*n })
Insert cell
time_to_radians = (ms) => { // noon = pi/2, increasing cw
const time = new Date(ms);
const ms_day = 24*60*60*1000;
return -1 * ( (3*Math.PI/2) - ((time - start_of_day(time)) / ms_day * (2*Math.PI)) );
}
Insert cell
start_of_day = (time) => new Date(time.getFullYear(), time.getMonth(), time.getDate())
Insert cell
time_lerp = (a, b, t) => {
const a_ms = a.getTime();
const b_ms = b.getTime();
return new Date(a_ms + ((b_ms - a_ms) * t));
}
Insert cell
valid_time = (d) => d.getTime() > 1000
// sunrise-sunset api gives times of 1sec past epoch when an event does not occur
Insert cell
world = ({
frigg_fjord: { latitude: 83.1166065, longitude: -41.9839829, label: 'Frigg Fjord, GRL' },
reykjavic: { latitude: 64.1334735, longitude: -21.9224815, label: 'Reykjavic, ISL'},
columbus: { latitude: 39.9828671, longitude: -83.1309138, label: 'Columbus OH, USA'},
san_francisco: { latitude: 37.7576793, longitude: -122.5076402, label: 'San Francisco CA, USA'},
singapore: { latitude: 1.3139961, longitude: 103.7041623, label: 'Singapore, SGP'},
panama_city: { latitude: 9.0813885, longitude: -79.5932249, label: 'Panama City, PAN'},
antananarivo: { latitude: -18.887626, longitude: 47.3724268, label: 'Antananarivo, MDG'},
wellington: { latitude: -41.2528753, longitude: 174.614173 , label: 'Wellington, NZL'},
punta_arenas: { latitude: -53.1417468, longitude: -70.9763068, label: 'Punta Arenas, CHL'},
mcmurdo_station: { latitude: -77.7240158, longitude: 164.7726887, label: 'McMurdo Station, ATA'},
})
Insert cell
hrs = [
'XXIV', 'I', 'II', 'III', 'IIII', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI',
'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII', 'XXIII',
]
Insert cell
Insert cell
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more