%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '32px'}}}%%
flowchart LR
A[Dec]-->B[date]-->C[time]-->D[snap]-->E[span]
click A "/dec"
click B "/dec/date"
click C "/dec/time"
click D "/dec/snap"
click E "/dec/span"
Dec Time
Introducing Declock, a timekeeping system that displays time in decimal days using math notation without the need for hours, minutes, or seconds.
2025+283
My website provides many examples of the Quarto publishing and the Dec measurement systems in action. I leverage Quarto support for the Observable data analysis and visualization system to create animated and interactive graphics like the bar📊charts clocks🕓, solar☀️terminator map🗺, Earth🌍orbit diagram, and daylight area chart below.
Dec times are measured in fractional days, often using metric prefixes like deci, centi, or milli. The top, middle, and bottom bars📊indicate the decidays, millidays, and centimillidays, respectively, of the time since the start, +, or until the end, -, of the day in the Dec time zone, , at the location of the red⭕️circle on the map🗺️beneath the bars📊.
To rotate the globe🌐in the Earth🌏orbit diagram, drag the red⭕️circle horizontally↔︎️or slide the red🔴dot on the daylight area chart vertically↕. The red⭕️circle’s vertical↕position determines the yearly daylight pattern visualized by the area chart. Shift the red—line on the area chart horizontally↔︎️to move the globe🌐along the ellipse of the Earth🌎orbit.
viewof bordertoggle = labelToggle(Inputs.toggle, "Border", false, "bordertoggle")
viewof gridtoggle = labelToggle(Inputs.toggle, "Grid", false, "gridtoggle")
viewof utctoggle = labelToggle(Inputs.toggle, "UTC", false, "utctoggle")
viewof suntoggle = labelToggle(Inputs.toggle, "Sun", false, "suntoggle")
rstbtn.node();Bar chart clocks
barChart = {
const W = 800;
const H = 88;
const barX = 1;
const firstBarY = 1;
const svg = d3
.create("svg")
.attr("width", W)
.attr("viewBox", [0, 0, W / 1.14, H]);
const xRange = [0, W - 100];
const scaleDD = d3.scaleLinear()
.domain([0, 10])
.range(xRange);
const scaleMandB = d3.scaleLinear()
.domain([0, 100])
.range(xRange);
// Background bars to show where 100% lies
svg.selectAll('.background')
.data([
'dd', "mils", 'beats'])
.enter()
.append('rect')
.attr('class', 'background timeBar')
.attr('width', W-100)
.attr('y', (d,i)=>i*30+firstBarY)
// Beats
svg
.append('rect')
.attr('class', 'timeBar')
.attr('y', firstBarY+60)
.attr('width', d => scaleMandB(Number(barBeats)))
svg
.append('rect')
.attr('class', 'timeBarFull')
.attr('y', firstBarY+60)
.attr('width', d => scaleMandB(barBeats))
// Cents/Mils
svg
.append('rect')
.attr('class', 'timeBar')
.attr('y', firstBarY)
.attr('width', d => scaleDD(Number(barDD)+Number(barMils)/100+Number(barBeats)/10000))
svg
.append('rect')
.attr('class', 'timeBarFull')
.attr('y', firstBarY)
.attr('width', d => scaleDD(barDD))
svg
.append('rect')
.attr('class', 'timeBar')
.attr('y', firstBarY+30)
.attr('width', d => scaleMandB(Number(barMils)+Number(barBeats)/100))
svg
.append('rect')
.attr('class', 'timeBarFull')
.attr('y', firstBarY+30)
.attr('width', d => scaleMandB(barMils))
// Cent ticks
svg.selectAll('.tickC')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickC')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', firstBarY+30)
.attr('height', d=>d%2===0? 8:5)
svg.selectAll('.tickB1')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickB1')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', d=>d%2===0? firstBarY+77:firstBarY+80)
.attr('height', d=>d%2===0? 8:5)
svg.selectAll('.tickC1')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickC1')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', d=>d%2===0? firstBarY+47:firstBarY+50)
.attr('height', d=>d%2===0? 8:5)
// Mil ticks
svg.selectAll('.tickM')
.data(d3.range(width > 500 ? 1 : 1, 10))
.enter()
.append('rect')
.attr('class', 'tickM')
.attr('x', d=>scaleDD(d)+barX)
.attr('y', firstBarY+20)
.attr('height', 6)
svg.selectAll('.tickLabel1')
.data(d3.range(width > 500 ? 1 : 1, 10))
.enter()
.append('text')
.attr('class', 'tickLabel1')
.attr('x', d=>scaleDD(d)+barX+.5)
.attr('y', firstBarY+18)
//.style("font-size", `{W < 550 ? 12 : W < 650 ? 14 : W < 750 ? 16 : W < 850 ? 18 : 20}px`)
.text(d=>d)
// Cent ticks
svg.selectAll('.tickC2')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickC2')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', firstBarY+10)
.attr('height', d=>d%2===0? 9:6)
// Beat ticks
svg.selectAll('.tickB')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickB')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', firstBarY+60)
.attr('height', d=>d%2===0? 9:6)
// Labels
svg.selectAll('.timeLabel')
.data([`+${barDD}`, `${barMils}`, `${barBeats}`])
.enter()
.append('text')
.attr('class', 'timeLabel')
.attr('x', barX+2)
.attr('y', (d,i)=>i*30+firstBarY+20)
.style("font-size", `${W < 300 ? 14 : W < 400 ? 16 : W < 500 ? 18 : W < 600 ? 20 : 22}px`)
.text(d=>d);
svg.attr("id", "topbar");
svg.attr('class', 'barclock')
return svg.node();
}
barChart1 = {
const W = 800;
const H = 88;
const barX = 1;
const firstBarY = 1;
const svg = d3
.create("svg")
.attr("width", W)
.attr("viewBox", [0, 0, W / 1.14, H]);
const xRange = [0, W - 100];
const scaleDD = d3.scaleLinear()
.domain([0, 10])
.range(xRange);
const scaleMandB = d3.scaleLinear()
.domain([0, 100])
.range(xRange);
// const scaleDek = d3.scaleLinear()
// .domain([0, 37])
// .range(xRange);
// Background bars to show where 100% lies
svg.selectAll('.background')
.data([
// 'dek', 'dotd',
'dd', "mils", 'beats'])
.enter()
.append('rect')
.attr('class', 'background timeBar')
.attr('width', W-100)
.attr('y', (d,i)=>i*30+firstBarY)
// Beats
svg
.append('rect')
.attr('class', 'timeBar')
.attr('y', firstBarY+60)
.attr('width', d => scaleMandB(Number(barBeatsN)))
svg
.append('rect')
.attr('class', 'timeBarFullN')
.attr('y', firstBarY+60)
.attr('width', d => scaleMandB(barBeatsN))
// Cents/Mils
svg
.append('rect')
.attr('class', 'timeBar')
.attr('y', firstBarY)
.attr('width', d => scaleDD(Number(barDDN)+Number(barMilsN)/100+Number(barBeatsN)/10000))
svg
.append('rect')
.attr('class', 'timeBarFullN')
.attr('y', firstBarY)
.attr('width', d => scaleDD(barDDN))
svg
.append('rect')
.attr('class', 'timeBar')
.attr('y', firstBarY+30)
.attr('width', d => scaleMandB(Number(barMilsN)+Number(barBeatsN)/100))
svg
.append('rect')
.attr('class', 'timeBarFullN')
.attr('y', firstBarY+30)
.attr('width', d => scaleMandB(barMilsN))
// Cent ticks
svg.selectAll('.tickC')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickC')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', firstBarY+30)
.attr('height', d=>d%2===0? 8:5)
svg.selectAll('.tickB1')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickB1')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', d=>d%2===0? firstBarY+77:firstBarY+80)
.attr('height', d=>d%2===0? 8:5)
svg.selectAll('.tickC1')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickC1')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', d=>d%2===0? firstBarY+47:firstBarY+50)
.attr('height', d=>d%2===0? 8:5)
// Mil ticks
svg.selectAll('.tickM')
.data(d3.range(width > 500 ? 1 : 1, 10))
.enter()
.append('rect')
.attr('class', 'tickM')
.attr('x', d=>scaleDD(d)+barX)
.attr('y', firstBarY+20)
.attr('height', 6)
svg.selectAll('.tickLabel1')
.data(d3.range(width > 500 ? 1 : 1, 10))
.enter()
.append('text')
.attr('class', 'tickLabel1')
.attr('x', d=>scaleDD(d)+barX+.5)
.attr('y', firstBarY+18)
//.style("font-size", `{W < 350 ? 12 : W < 450 ? 14 : W < 550 ? 16 : W < 650 ? 18 : 20}px`)
.text(d=>d)
// Cent ticks
svg.selectAll('.tickC2')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickC2')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', firstBarY+10)
.attr('height', d=>d%2===0? 9:6)
// Beat ticks
svg.selectAll('.tickB')
.data(d3.range(width > 500 ? 10 : 10, 100))
.enter()
.append('rect')
.attr('class', 'tickB')
.attr('x', d=>scaleDD(d/10)+barX)
.attr('y', firstBarY+60)
.attr('height', d=>d%2===0? 9:6)
// Labels
svg.selectAll('.timeLabel')
.data([`-${barDDN}`, `${barMilsN}`, `${barBeatsN}`])
.enter()
.append('text')
.attr('class', 'timeLabel')
.attr('x', barX+2)
.attr('y', (d,i)=>i*30+firstBarY+20)
.style("font-size", `${W < 300 ? 14 : W < 400 ? 16 : W < 500 ? 18 : W < 600 ? 20 : 22}px`)
.text(d=>d);
svg.attr("id", "btmbar");
svg.attr('class', 'barclock')
return svg.node();
}viewof select = Inputs.select(
projections, {format: x => x.name, value: projections.find(t => t.name === "Equirectangular (plate carrée)")})table = createTable([
{ Milliparallel: 500, Millimeridian: 0 },
], { headerEditable: false, appendRows: false })
// {Point: 0, Milliparallel: `${Math.floor(long2turn(Place_A[0], 3))}`, Millimeridian: `${Math.floor(lati2turn(Place_A[1], 3))}`, Milliwindrose: `${Math.floor(lati2turn(coor2bear(Place_A, Place_B)))}`},
// {Point: 1, Milliparallel: `${Math.floor(long2turn(Place_B[0], 3))}`, Millimeridian: `${Math.floor(lati2turn(Place_B[1], 3))}`, Milliwindrose: `${Math.floor(lati2turn(coor2bear(Place_B, Place_A)))}`},
// ], {headerEditable: false, appendRows: false})Longitude latitude map
Daylight area chart
// https://observablehq.com/@dbridges/visualizing-seasonal-daylight
app = {
const svg = d3.select(DOM.svg(majwid, majwid / 1000 * (width < 300 ? 286 : width < 310 ? 292 : width < 320 ? 298 : width < 330 ? 304 : width < 340 ? 310 : width < 350 ? 316 : width < 360 ? 322 : width < 370 ? 328 : width < 380 ? 334 : width < 390 ? 340 : width < 400 ? 346 : width < 410 ? 362 : width < 420 ? 374 : width < 430 ? 382 : width < 440 ? 386 : width < 450 ? 390 : width < 460 ? 394 : width < 470 ? 398 : width < 480 ? 402 : width < 490 ? 406 : width < 500 ? 418 : width < 510 ? 422 : width < 520 ? 426 : width < 530 ? 430 : width < 540 ? 434 : width < 550 ? 438 : width < 560 ? 442 : width < 570 ? 446 : width < 580 ? 456 : width < 590 ? 460 : width < 600 ? 470 : width < 610 ? 480 : width < 650 ? 490 : width < 700 ? 490 : width < 734 ? 490 : width < 768 ? 490 : width < 769 ? 320 : width < 800 ? 340 : width < 900 ? 350 : width < 1000 ? 360 : width < 1050 ? 420 : width < 1100 ? 415 : width < 1150 ? 413 : width < 1200 ? 410 : width < 1300 ? 380 : width < 1400 ? 360 : width < 1450 ? 340 : width < 1500 ? 320 : width < 1550 ? 300 : width < 1575 ? 290 : width < 1600 ? 280 : width < 1650 ? 270 : width < 1700 ? 260 : width < 1750 ? 258 : width < 1800 ? 254 : width < 1850 ? 250 : width < 1800 ? 248 : width < 1900 ? 246 : width < 1950 ? 244 : width < 2000 ? 242 : width < 2050 ? 240 : width < 2100 ? 230 : width < 2150 ? 225 : width < 2200 ? 220 : width < 2250 ? 215 : width < 2300 ? 210 : width < 2350 ? 205 : width < 2400 ? 200 : width < 2450 ? 195 : width < 2500 ? 189 : width < 2550 ? 190 : width < 2600 ? 186 : width < 2650 ? 184 : width < 2700 ? 182 : width < 2750 ? 180 : width < 2800 ? 178 : width < 2850 ? 174 : width < 2900 ? 170 : width < 3000 ? 166 : width < 3100 ? 162 : width < 3200 ? 156 : width < 3300 ? 152 : width < 3400 ? 146 : width < 3500 ? 140 : width < 3600 ? 134 : width < 3700 ? 130 : width < 3800 ? 126 : width < 3800 ? 122 : width < 3900 ? 118 : width < 4000 ? 114 : width < 4100 ? 110 : width < 4200 ? 106 : width < 4300 ? 104 : width < 4400 ? 102 : width < 4500 ? 100 : width < 4600 ? 98 : width < 4700 ? 96 : width < 4800 ? 94 : width < 4800 ? 92 : width < 4900 ? 90 : width < 5100 ? 88 : width < 5200 ? 86 : width < 5300 ? 84 : width < 5450 ? 82 : 84)));
svg.style("user-select", "none").style("-webkit-user-select", "none").attr("id", "daylightapp");
const margin = {top: 0, left: 16, right: 16, bottom: 0, inner: 32};
const contentWidth = majwid - margin.left - margin.right - margin.inner;
const columnWidth = contentWidth / 2;
let selection = {
date: this.value != null ? this.value.date : new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().getHours()),
hour: this.value != null ? this.value.hour : new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().getHours()).getHours()
}
const renderPlot = () => {
svg.selectAll("#plot *").remove();
svg.select("#plot").call(daylightPlot, {
vizwidth: columnWidth / (width < 300 ? 1.07 : width < 325 ? 1.08 : width < 350 ? 1.09 : width < 400 ? 1.1 : width < 450 ? 1.09 : width < 500 ? 1.08 : width < 540 ? 1.07 : width < 550 ? 1.06 : width < 560 ? 1.05 : width < 580 ? 1.04 : width < 590 ? 1.03 : width < 600 ? 1.02 : width < 610 ? 1.01 : width < 625 ? 1 : width < 650 ? 1 : width < 675 ? 1 : width < 700 ? 1 : width < 725 ? 1 : width < 750 ? 1 : width < 768 ? 1 : width < 769 ? 1 : width < 800 ? .96 : width < 825 ? .96 : width < 850 ? .94 : width < 875 ? .96 : width < 900 ? .98 : width < 925 ? 1 : width < 950 ? 1.02 : width < 975 ? 1.04 : width < 1000 ? 1.06 : width < 1025 ? .95 : width < 1050 ? .95 : width < 1100 ? .975 : width < 1150 ? 1 : width < 1200 ? 1.05 : width < 1225 ? 1.1 : width < 1250 ? 1.15 : width < 1275 ? 1.2 : width < 1300 ? 1.25 : width < 1350 ? 1.3 : width < 1400 ? 1.35 : width < 1450 ? 1.4 : width < 1500 ? 1.45 : width < 1600 ? 1.5 : width < 1650 ? 1.55 : width < 1700 ? 1.5 : width < 1800 ? 1.5 : width < 1900 ? 1.45 : width < 2000 ? 1.45 : width < 2200 ? 1.45 : width < 2400 ? 1.45 : width < 2600 ? 1.45 : width < 2800 ? 1.45 : width < 3000 ? 1.45 : width < 3200 ? 1.45 : width < 3400 ? 1.45 : width < 3600 ? 1.5 : width < 3800 ? 1.55 : width < 4000 ? 1.6 : width < 4700 ? 1.6 : width < 4800 ? 1.6 : width < 4900 ? 1.6 : width < 5000 ? 1.6 : width < 5600 ? 1.6 : 1.6),
height: majwid * (width < 300 ? 1.12 : width < 340 ? 1.11 : width < 350 ? 1.1 : width < 360 ? 1.09 : width < 370 ? 1.08 : width < 380 ? 1.07 : width < 390 ? 1.06 : width < 400 ? 1.05 : width < 425 ? 1.04 : width < 450 ? 1.03 : width < 475 ? 1.02 : width < 500 ? 1.01 : width < 550 ? 1 : width < 600 ? 1 : width < 650 ? 1 : width < 700 ? 1 : width < 750 ? 1 : width < 769 ? 1 : width < 800 ? .93 : width < 850 ? .91 : width < 900 ? .92 : width < 950 ? .9 : width < 1000 ? .9 : width < 1050 ? .9 : width < 1100 ? .9 : width < 1200 ? 0.91 : width < 1300 ? 0.92 : width < 1400 ? .93 : width < 1500 ? .95 : width < 1600 ? .97 : width < 1700 ? .98 : width < 1800 ? .99 : width < 4800 ? 1 : width < 5600 ? 1 : 1),
year: 2022,
latitude: location[1],
defaultDate: selection.date,
defaultHour: selection.hour
})
}
const renderSolarSystem = () => {
svg.selectAll("#solar-system *").remove();
svg.selectAll("#solar-system").call(solarSystem,
majwid * (width < 350 ? .922 : width < 350 ? .92 : width < 450 ? .89 : width < 532 ? .89 : width < 769 ? .94 : width < 1000 ? .9 : width < 1100 ? .91 : width < 1200 ? .92 : width < 1300 ? .92 : width < 1400 ? .92 : width < 1500 ? .93 : width < 1600 ? .94 : .96),
location,
selection.date,
selection.hour,
window.darkmode);
}
// const renderGlobe = () => {
// svg.selectAll("#globe *").remove();
// svg.selectAll("#globe").call(globe, { vizwidth: columnWidth / 1.28, location, ...selection });
// }
const setSelection = (newSelection, forceRender = false) => {
const prev = {...selection};
selection = newSelection;
svg.node().value = selection;
this.value = selection;
set(viewof selectedDate, selection.date);
set(viewof selectedHour, selection.hour);
if (forceRender) {
renderPlot();
renderSolarSystem();
// renderGlobe();
} else if (prev.hour !== selection.hour || prev.date !== selection.date) {
renderSolarSystem();
// renderGlobe();
}
}
const plot = svg.append("g")
.attr("id", "plot")
.attr("transform", `translate(
${margin.left + (width < 275 ? -16 : width < 300 ? -14 : width < 325 ? -12 : width < 350 ? -9 : width < 375 ? -8 : width < 400 ? -4 : width < 425 ? 2 : width < 450 ? 4 : width < 475 ? 6 : width < 500 ? 11 : width < 550 ? 16 : width < 600 ? 18 : width < 650 ? 20 : width < 700 ? 20 : width < 769 ? 21 : width < 800 ? 63 : width < 900 ? 83 : width < 1000 ? 100 : width < 1100 ? 140 : width < 1200 ? 135 : width < 1300 ? 130 : width < 1400 ? 124 : width < 1500 ? 112 : width < 1600 ? 90 : width < 1700 ? 80 : width < 1800 ? 30 : width < 1900 ? 20 : width < 2000 ? 10 : width < 2100 ? 5 : 0)},
${width < 300 ? -90 : width < 325 ? -90 : width < 350 ? -90 : width < 375 ? -90 : width < 400 ? -90 : width < 425 ? -90 : width < 450 ? -90 : width < 500 ? -91 : width < 550 ? -92 : width < 600 ? -92 : width < 650 ? -91 : width < 675 ? -91 : width < 700 ? -91 : width < 750 ? -91 : width < 760 ? -91 : width < 768 ? -91 : width < 769 ? -170 : width < 780 ? -170 : width < 790 ? -170 : width < 800 ? -174 : width < 825 ? -178 : width < 850 ? -184 : width < 900 ? -186 : width < 950 ? -186 : width < 975 ? -186 : width < 992 ? -186 : width < 1000 ? -180 : width < 1025 ? -190 : width < 1050 ? -192 : width < 1075 ? -196 : width < 1100 ? -200 : width < 1150 ? -200 : width < 1200 ? -200 : width < 1250 ? -190 : width < 1300 ? -190 : width < 1320 ? -180 : width < 1340 ? -180 : width < 1360 ? -180 : width < 1380 ? -185 : width < 1400 ? -185 : width < 1420 ? -185 : width < 1450 ? -200 : width < 1480 ? -205 : width < 1500 ? -210 : width < 1520 ? -210 : width < 1540 ? -220 : width < 1560 ? -224 : width < 1580 ? -228 : width < 1600 ? -235 : width < 1620 ? -235 : width < 1640 ? -235 : width < 1660 ? -245 : width < 1680 ? -250 : width < 1700 ? -255 : width < 1720 ? -265 : width < 1740 ? -270 : width < 1760 ? -280 : width < 1780 ? -285 : width < 1800 ? -290 : width < 1820 ? -295 : width < 1840 ? -305 : width < 1860 ? -310 : width < 1880 ? -315 : width < 1900 ? -320 : width < 1925 ? -330 : width < 1950 ? -335 : width < 1975 ? -340 : width < 2000 ? -350 : width < 2025 ? -360 : width < 2050 ? -365 : width < 2075 ? -370 : width < 2100 ? -380 : width < 2125 ? -380 : width < 2150 ? -385 : width < 2175 ? -395 : width < 2200 ? -410 : width < 2225 ? -400 : width < 2250 ? -415 : width < 2275 ? -420 : width < 2300 ? -430 : width < 2325 ? -430 : width < 2350 ? -440 : width < 2375 ? -450 : width < 2400 ? -460 : width < 2425 ? -450 : width < 2450 ? -465 : width < 2475 ? -470 : width < 2500 ? -482 : width < 2525 ? -485 : width < 2550 ? -500 : width < 2575 ? -510 : width < 2600 ? -520 : width < 2625 ? -520 : width < 2650 ? -530 : width < 2675 ? -545 : width < 2700 ? -555 : width < 2725 ? -555 : width < 2750 ? -565 : width < 2775 ? -575 : width < 2800 ? -585 : width < 2825 ? -590 : width < 2850 ? -595 : width < 2875 ? -600 : width < 2900 ? -610 : width < 2925 ? -610 : width < 2950 ? -620 : width < 2975 ? -630 : width < 3000 ? -645 : width < 3025 ? -640 : width < 3050 ? -660 : width < 3075 ? -670 : width < 3100 ? -680 : width < 3125 ? -670 : width < 3150 ? -685 : width < 3175 ? -695 : width < 3200 ? -705 : width < 3225 ? -705 : width < 3250 ? -720 : width < 3275 ? -730 : width < 3300 ? -740 : width < 3325 ? -730 : width < 3350 ? -740 : width < 3375 ? -750 : width < 3400 ? -760 : width < 3425 ? -750 : width < 3450 ? -760 : width < 3475 ? -770 : width < 3500 ? -780 : width < 3525 ? -770 : width < 3550 ? -780 : width < 3575 ? -790 : width < 3600 ? -800 : width < 3625 ? -800 : width < 3650 ? -810 : width < 3675 ? -815 : width < 3700 ? -825 : width < 3725 ? -825 : width < 3750 ? -840 : width < 3775 ? -850 : width < 3800 ? -860 : width < 3825 ? -830 : width < 3850 ? -840 : width < 3875 ? -850 : width < 3900 ? -855 : width < 3925 ? -850 : width < 3950 ? -860 : width < 3975 ? -870 : width < 4000 ? -875 : width < 4025 ? -865 : width < 4050 ? -875 : width < 4075 ? -890 : width < 4100 ? -900 : width < 4125 ? -880 : width < 4150 ? -890 : width < 4175 ? -895 : width < 4200 ? -905 : width < 4225 ? -910 : width < 4250 ? -920 : width < 4275 ? -930 : width < 4300 ? -940 : width < 4325 ? -950 : width < 4350 ? -960 : width < 4400 ? -970 : width < 4425 ? -975 : width < 4450 ? -980 : width < 4475 ? -985 : width < 4500 ? -990 : width < 4525 ? -1000 : width < 4550 ? -1005 : width < 4575 ? -1020 : width < 4600 ? -1030 : width < 4650 ? -1030 : width < 4700 ? -1055 : width < 4750 ? -1060 : width < 4800 ? -1080 : width < 4850 ? -1070 : width < 4900 ? -1085 : width < 4950 ? -1095 : width < 5000 ? -1115 : width < 5025 ? -1130 : width < 5050 ? -1140 : width < 5075 ? -1150 : width < 5100 ? -1160 : width < 5125 ? -1155 : width < 5150 ? -1160 : width < 5200 ? -1180 : width < 5225 ? -1175 : width < 5250 ? -1185 : width < 5275 ? -1195 : width < 5300 ? -1210 : width < 5325 ? -1205 : width < 5350 ? -1210 : width < 5375 ? -1220 : width < 5400 ? -1235 : width < 5420 ? -1245 : width < 5440 ? -1255 : width < 5450 ? -1265 : width < 5460 ? -1280 : width < 5480 ? -1295 : width < 5500 ? -1310 : width < 5525 ? -1315 : width < 5550 ? -1325 : width < 5575 ? -1335 : width < 5600 ? -1345 : width < 5625 ? -1360 : width < 5650 ? -1370 : width < 5675 ? -1380 : width < 5700 ? -1390 : width < 5720 ? -1400 : width < 5740 ? -1410 : width < 5760 ? -1415 : width < 5780 ? -1430 : width < 5800 ? -1440 : width < 5825 ? -1460 : width < 5850 ? -1470 : width < 5875 ? -1480 : -1495})`);
svg.append("g")
.attr("id", "solar-system")
.attr("transform", `translate(
${margin.left + (width < 300 ? -1 : width < 350 ? 0 : width < 400 ? 3 : width < 425 ? 4 : width < 450 ? 5 : width < 475 ? 6 : width < 500 ? 7 : width < 516 ? 8 : width < 532 ? 9 : width < 600 ? -3 : width < 650 ? -2 : width < 700 ? -1 : width < 750 ? 0 : width < 769 ? 0 : width < 800 ? 30 : width < 900 ? 32 : width < 1000 ? 36 : width < 1100 ? 80 : width < 1200 ? 74 : width < 1225 ? 74 : width < 1250 ? 72 : width < 1275 ? 68 : width < 1300 ? 66 : width < 1400 ? 58 : width < 1450 ? 40 : width < 1500 ? 45 : width < 1600 ? 25 : width < 1700 ? 0 : width < 1750 ? -54 : width < 1800 ? -52 : width < 1900 ? -60 : width < 2000 ? -70 : width < 2100 ? -75 : width < 2200 ? -80 : width < 2300 ? -84 : width < 2400 ? -88 : width < 2500 ? -98 : width < 2600 ? -108 : width < 2700 ? -118 : width < 2800 ? -120 : width < 2900 ? -122 : width < 3000 ? -124 : width < 3100 ? -126 : width < 3200 ? -128 : width < 3300 ? -130 : width < 3400 ? -132 : width < 3500 ? -134 : width < 3600 ? -136 : width < 3700 ? -138 : width < 3800 ? -140 : width < 3900 ? -142 : width < 4000 ? -144 : width < 4100 ? -146 : width < 4200 ? -148 : width < 4300 ? -150 : width < 4400 ? -155 : width < 4500 ? -160 : width < 4600 ? -165 : width < 4700 ? -170 : width < 4800 ? -175 : width < 4900 ? -180 : width < 5000 ? -185 : width < 5200 ? -190 : width < 5400 ? -190 : width < 5600 ? -190 : width < 5800 ? -190 : -200)},
${width < 280 ? 66 : width < 290 ? 70 : width < 300 ? 74 : width < 310 ? 80 : width < 320 ? 86 : width < 330 ? 92 : width < 340 ? 98 : width < 350 ? 104 : width < 360 ? 110 : width < 370 ? 114 : width < 380 ? 118 : width < 390 ? 122 : width < 400 ? 126 : width < 410 ? 134 : width < 420 ? 142 : width < 425 ? 146 : width < 430 ? 150 : width < 440 ? 158 : width < 450 ? 162 : width < 460 ? 164 : width < 470 ? 170 : width < 480 ? 176 : width < 490 ? 180 : width < 500 ? 192 : width < 510 ? 200 : width < 520 ? 206 : width < 530 ? 212 : width < 540 ? 218 : width < 550 ? 224 : width < 560 ? 230 : width < 570 ? 236 : width < 580 ? 240 : width < 590 ? 250 : width < 600 ? 260 : width < 610 ? 265 : width < 620 ? 270 : width < 630 ? 275 : width < 640 ? 280 : width < 650 ? 290 : width < 660 ? 295 : width < 670 ? 300 : width < 680 ? 305 : width < 690 ? 310 : width < 700 ? 315 : width < 710 ? 320 : width < 720 ? 325 : width < 730 ? 330 : width < 740 ? 335 : width < 760 ? 340 : width < 768 ? 350 : width < 769 ? 270 : width < 770 ? 290 : width < 780 ? 290 : width < 800 ? 290 : width < 825 ? 310 : width < 850 ? 325 : width < 875 ? 335 : width < 900 ? 340 : width < 950 ? 360 : width < 975 ? 370 : width < 1000 ? 380 : width < 1050 ? 450 : width < 1100 ? 470 : width < 1150 ? 485 : width < 1200 ? 490 : width < 1250 ? 470 : width < 1300 ? 480 : width < 1350 ? 475 : width < 1400 ? 480 : width < 1440 ? 470 : width < 1450 ? 470 : width < 1480 ? 470 : width < 1500 ? 470 : width < 1520 ? 470 : width < 1540 ? 475 : width < 1550 ? 470 : width < 1560 ? 465 : width < 1600 ? 460 : width < 1620 ? 450 : width < 1640 ? 460 : width < 1660 ? 460 : width < 1680 ? 460 : width < 1700 ? 470 : width < 1720 ? 480 : width < 1780 ? 490 : width < 1800 ? 500 : width < 1820 ? 520 : width < 1880 ? 530 : width < 1920 ? 550 : width < 1970 ? 560 : width < 2000 ? 570 : width < 2100 ? 580 : width < 2200 ? 590 : width < 2300 ? 600 : width < 2400 ? 610 : width < 2500 ? 635 : width < 2600 ? 670 : width < 2700 ? 690 : width < 2800 ? 710 : width < 2900 ? 720 : width < 2950 ? 730 : width < 3000 ? 750 : width < 3100 ? 775 : width < 3200 ? 790 : width < 3250 ? 790 : width < 3300 ? 805 : width < 3400 ? 810 : width < 3450 ? 815 : width < 3500 ? 820 : width < 3550 ? 815 : width < 3600 ? 825 : width < 3700 ? 835 : width < 3750 ? 830 : width < 3800 ? 835 : width < 3850 ? 830 : width < 3900 ? 840 : width < 4000 ? 840 : width < 4100 ? 850 : width < 4200 ? 860 : width < 4250 ? 870 : width < 4300 ? 880 : width < 4325 ? 890 : width < 4350 ? 900 : width < 4400 ? 905 : width < 4500 ? 910 : width < 4550 ? 925 : width < 4600 ? 930 : width < 4650 ? 935 : width < 4700 ? 940 : width < 4800 ? 950 : width < 4850 ? 950 : width < 4900 ? 960 : width < 4950 ? 970 : width < 5000 ? 980 : width < 5050 ? 985 : width < 5100 ? 990 : width < 5150 ? 1005 : width < 5250 ? 1035 : width < 5300 ? 1010 : width < 5325 ? 1035 : width < 5350 ? 1060 : width < 5450 ? 1075 : width < 5500 ? 1080 : width < 5550 ? 1095 : width < 5600 ? 1110 : 1200})`);
// svg.append("g")
// .attr("id", "globe")
// .attr("transform", `translate(${margin.left + margin.inner + columnWidth / 2 + 20}, ${margin.top + height / 1.2 + 4})`);
setSelection(selection, true);
const handleDateHourChange = ({ target, detail: { date, hour }}) => {
if (date != null && hour != null) setSelection({...selection, date, hour});
}
svg.node().addEventListener(EventType.DateHourChange, handleDateHourChange, false);
return svg.node();
}The red—line indicates a “day of year” (doy), , and the red🔴dot denotes a “time of day” (tod): . A doy identifies a day in a year like a Gregorian calendar month and “day of month” (dom). A tod specifies a point in a day like an “hour minute second” (hms) triplet. Together, a doy and tod can form a “annual day aggregate” (ada): .
\[\begin{split} \text{ada\;\,} & = \text{doy} + \text{tod} \\ \lfloor\text{ada}\rfloor & = \text{doy} \\ \end{split}\]
As their names suggest, doys and adas are measured in days. The measurement unit of a tod can be a day or a submultiple of a day. By changing how a decimal tod is measured, we can shift its decimal separator or turn it into an integer. The tods along the y-axis of the area chart are integers because they have three digits and are measured in millidays.
Epochal day aggregate (eda)
To obtain a tod from a ada, we can keep the remainder after dividing by one to isolate the decimal part of the quotient: mod 1 = . We can use this same approach to separate a tod from an “epochal day aggregate” (eda): mod 1 = . The current eda tells us how many days have passed since the Dec epoch.
\[\begin{split} \text{tod} &= \text{ada mod } 1 &&= \text{eda mod } 1 \\ \text{tod} &= \text{ada} - \text{doy} &&= \text{eda} - \lfloor\text{eda}\rfloor \end{split}\]
The Dec doe equations We can describe an “epochal year aggregate” (eya) instead of a yoe,
When provided with an “epochal day aggregate” (eda) instead of a doe, the Dec date equations return a “annual day aggregate” (ada) instead of a doy. While does and doys are integers, edas and adas each have a decimal part called a “time of day” (tod). We can obtain an eda by passing a yoe and ada to the Dec eda equations or by summing a doe and a tod. isolate a tod from an eda or ada, and convert between a doe and eda or a doy and ada.
\[\text{eda} = \text{coe}\times146097 + \text{yoc}\times365 + \lfloor\frac{\text{yoc}}{4}\rfloor - \lfloor\frac{\text{yoc}}{100}\rfloor + \text{ada}\]
\[\text{tod} = \text{eda mod } 1 = \text{ada mod } 1\]
\[\text{doe} = \lfloor \text{eda} \rfloor = \text{eda} - \text{tod}\]
\[\text{doy} = \lfloor \text{ada} \rfloor = \text{ada} - \text{tod}\]
\[\text{eda} = \text{doe} + \text{tod}\]
\[\text{ada} = \text{doy} + \text{tod}\]
UNIX time equation
Similarly, UNIX time tallies the seconds since the UNIX epoch, which is exactly 719468 days after the Dec epoch. To get the tod in Zone 0, the Dec time zone that is in between the two leftmost vertical lines on the map🗺️, we can divide UNIX time by the number of seconds in a day and then keep the remainder after dividing the resulting days by one:
\[\text{tod} = \text{unix} \div 86400 \text{ mod } 1\]
Julian time equation
Julian dates track the days since the beginning of the Julian period and thus are akin to edas. We can produce a Zone 5 tod from a Julian date simply by keeping the remainder after dividing by one. If we want a Zone 0 tod instead, we should add 5 decidays to the Julian date before converting it to a tod to ensure that the final result is less than one day:
\[\text{tod} = (\text{julian} + 0.5) \text{ mod } 1\]
Hour minute second
We can also obtain a Zone 0 tod from a Coordinated Universal Time (UTC) hms triplet by summing its components after converting them to fractional days, as shown in the equation below. The computer programming code in the tabset panel beneath the equation compares tods derived from UTC and UNIX time as Quarto was rendering this webpage.
\[\text{tod} = \frac{\text{hour}}{24} + \frac{\text{minute}}{1440} + \frac{\text{second}}{86400}\]
hms <- as.POSIXlt(Sys.time(), tz = "UTC")
hms$hour / 24 +
hms$min / 1440 +
hms$sec / 86400[1] 0.9040864
(as.numeric(as.POSIXct(hms)) / 86400) %% 1[1] 0.9040864
The equations below convert UNIX time or a Zone 0 tod into the three components of an hms triplet: the “hour of day” (hod), “minute of hour” (moh), and “second of minute” (som), using a “daily second aggregate” (dsa) and “hourly second aggregate” (hsa). While both count seconds, dsas start at midnight and hsas begin at the top of the hour.
\[\begin{split} \text{dsa} & = \text{tod} \times 86400 = \text{unix mod } 86400 \\ \text{hsa} & = \text{dsa mod } 3600 \\ \text{hod} & = \lfloor \text{dsa} \div 3600 \rfloor \\ \text{moh} & = \lfloor \text{hsa} \div 60 \rfloor \\ \text{som} & = \lfloor \text{hsa mod } 60 \rfloor \end{split}\]
dsa <- (as.numeric(as.POSIXct(Sys.time())) / 86400) %% 1 * 86400
hsa <- dsa %% 3600
sapply(c(dsa %/% 3600, hsa %/% 60, hsa %% 60), as.integer)[1] 21 41 53
Universal time offset
The Global Positioning System, BeiDou, and Galileo global navigation satellite systems along with most — if not all — programming languages do not account for leap seconds, which appears to be for the best given that leap seconds will be abolished by 2035. The goal of leap seconds is to keep UTC within 25/24 centimillidays (cmds) of Universal Time (UT).
Instead of leap seconds, Dec matches UT using a “universal time offset” (uto). With the leap second insertion dates provided by the International Earth Rotation and Reference Systems Service, we can approximate the uto that yields UT when added to the Zone 0 tod on the Dec date chosen by the range🎚️inputs below: ÷ 8640 = .
Rounded offset decimal
Of the twenty eight utos that can be shown in the equation above, one is an integer, one is a terminating decimal, and the rest are repeating decimals. To express a repeating decimal uto, Dec uses an irreducible fraction that is called an “exact offset fraction” (eof) when by itself or a “rounding error fraction” (ref) if it follows a “rounded offset decimal” (rod).
In the equation below, the uto is the minuend, the rod is the subtrahend, and the ref is the difference. Dec uses the term minuend expansion to describe the replacement of a minuend with a subtrahend and a difference. By replacing a repeating decimal uto with a rod and a ref, we can show the initial digits of the uto as a decimal and the rest as a fraction.
\[\text{uto} - \text{rod} = \text{ref}\]
Use the first three range🎚️inputs below to select an hms triplet to be converted to decidays, plugged into the equation above as the uto, rounded to the number of digits chosen by the fourth range🎚️input, and inserted into the equation as the rod. Once the left-hand side of the equation is complete, we can solve it to get the ref: – = .
Time zone offset
In Dec, a uto can be any type of number, a “time zone offset” (tzo) is an integer, a “solar time offset” (sto) is a terminating decimal, and an eof is a repeating decimal. If we derived deciday offsets from all 86400 of the hms triplets that can be selected by the range🎚️inputs above, we would have 10 tzos, 3190 stos, and 83200 eofs or rod and ref pairs.
Coordinated Universal Time (UTC)
When we do the same to the 38 UTC offsets, we get only eofs or rod and ref pairs unless the number of leaps seconds included is zero or a multiple of 27. If the leap second count is zero or a multiple of 8640, we will get 3 tzos, 9 stos, and 26 eofs or rod and ref pairs. The 3 tzos will be stos if the number of leap seconds is a multiple of 27 but not 8640.
There are 14 negative and 24 positive UTC offsets. The UTC time zone with the most negative offset is completely uninhabited. The bar📊chart below visualizes Socioeconomic Data and Applications Center data from 2020 regarding the population of each UTC time zone. The vast majority of all people live in UTC time zones with positive offsets.
// https://observablehq.com/@mattdzugan/population-by-time-zone-creating-a-dataset
viewof sortParams = Inputs.form([
Inputs.toggle({ label: "Sort ascending" }),
Inputs.toggle({ label: "Sort by offset" }),
])// https://observablehq.com/@mattdzugan/population-by-time-zone-creating-a-dataset
Plot.plot({
width: Math.min(width, 1000),
height: 100 + Math.min(width / 25, 20),
marginBottom: 50,
style: `overflow: visible;font-size:${15 + Math.min(width / 90, 11)}px;`,
color: {scheme: "RdBu", className: "barPlotLegend"},
marginLeft: 80 + Math.min(width / 100, 20),
y: { label: null },
x: { grid: true, label: "Population (billions)", labelOffset: 45 + Math.min(width / 90, 22), transform: d => d / 1e9 },
marks: [
Plot.barX(sortedPop, {x: "pop", y: "Sign", fill: "Offset", stroke: "black", tip: true }),
]
})Negative UTC offsets only exist in the Americas and islands in the Atlantic and Pacific Oceans. Therefore, the bar📊chart above is essentially comparing the Americas to the rest of the world. According to 2021 United Nations Department of Economic and Social Affairs data, about one billion out of a total of almost eight billion people live in the Americas.
Whenever a negative offset is associated with a Dec date, a tod, or both a date and a tod, Dec will add one day to the date and ten decidays to the offset without modifying the tod. This typically occurs after the conversion of an hms triplet to a tod or a “year month day” (ymd) triplet to a Dec date. As a result, all Dec dates and tods have positive offsets.
Dec will not change a negative offset or its associated doy if the result of adding one day to the doy is uncertain. This uncertainly can only exist if we do not know whether a doy that is equal to 364 belongs to a common or leap year. The day after Day 364 of a common year is Day 0 of the subsequent year. In a leap year, Day 364 precedes Day 365.
Day of week
Even though it has no effect on the tod, adding one day to the doy also increments the “day of month” (dom) and “day of week” (dow) shown by Dec. The table below shows how someone accustomed to a negative offset could intrepret Dec dow numbers. From the perspective of a negative offset user, the dom and dow in Dec will be one day ahead.
| Saturday | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday |
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| -7 | -6 | -5 | -4 | -3 | -2 | -1 |
The one day difference between positive and negative offsets may make Dec dow numbers more intuitive than POSIX dow numbers for people who consider Sunday to be the first dow. According to the Common Locale Data Repository and a 2023 population ranking, over 89% of people in the Americas live in a country that starts the week on Sunday.
Longitude and offsets
In Dec, offsets are closely related to longitude. Dec measures longitude in parallels (λ) or submultiples of λ like deciparallels (dλ). A tzo is essentially a dλ longitude that had its decimal part removed via rounding, flooring, truncation, or ceiling. Whereas tzos have one digit, deciday tods and stos typically have up to four digits after the decimal separator.
The fourth digit in the decimal part of any current deciday tod increments 105 times per day, 100 times per milliday, or once per beat (iob), which is the lower bound of the normal resting heart rate of an adult. For everyday life, we should limit the length of tods to the three digits needed to show millidays (md) or the five digits required to display beats (b).
When the current tod has seven digits, the sixth digit changes too quickly to be read out loud and the seventh changes so fast that it appears as a blur. Near the Equator, a longitude that has seven digits is accurate to within about ten zems (z) or four meters, which is roughly the length of a subcompact car or the width of a U-shaped living room layout.
The Equator is approximately (~) 103 millitaurs (mc), ~4 × 104 kilometers (km), or ~105 kilozems (kz) long. If we move 1 mc, 40 km, or 100 kz to the East or West on or near the Equator, our sto will change by ~1 md, ~1.44 minutes, or ~100 b and our longitude will shift by ~0.36 degrees, ~1 milliparallel (mλ), ~21.6 arcminutes, or ~100 arcbeats (ab).
For precise geopositioning, it may be helpful to show geographic coordinates in submultiples of ab, but we are unlikely to benefit from units smaller than md when displaying the solar time or estimates of what the tod will be when the Sun rises, reaches its zenith, or sets on a given day. By default, Dec uses three digits to show each solar time and sto.
Equation of time
The two types of solar time are “mean solar time” (mst) and “apparent solar time” (ast). To calculate mst, we keep only the decimal part of the sum of 0.95, the Zone 0 tod measured in days, and our longitude measured in λ. If we want ast instead of mst, the sum needs to include the result of plugging the “time of year” (toy) into the “equation of time” (eot).
\[\text{toy} = \text{ada} \div \text{n}\]
\[\text{mst} = (0.95 + \text{tod} + \lambda) \text{ mod } 1\]
\[\text{ast} = (0.95 + \text{tod} + \lambda + \text{eot(toy)}) \text{ mod } 1\]
To obtain the toy, we divide the ada by the number of days in the year (n). If we use trigonometric functions that are designed to work with radians, we will have to multiply the toy by \(2\pi\) or \(\tau\). We do not need to modify the toy before passing it to the eot() function defined below because its trigonometric functions expect turns instead of radians.
\[\begin{split} \text{eot(toy)} & = \beta_0 \\ & + \beta_1 \times \text{costau(toy)} \\ & + \beta_2 \times \text{costau(2} \times \text{toy)} \\ & + \beta_3 \times \text{sintau(toy)} \\ & + \beta_4 \times \text{sintau(2} \times \text{toy)} \end{split}\]
The code above compares two functions which return both the sine and cosine of their input. The sincos() function assumes it will receive radians and the sincostau() works with turns. In general, working with turns is more convenient and intuitive. The examples above demonstrate that using turns instead of radians can yield more accurate results.
The values below are the coefficients of a model adapted from the National Oceanic and Atmospheric Administration (NOAA) General Solar Position Calculations. Before fitting the model to NOAA yearly solar data, we need to convert eot(toy) values from minutes to centidays, combine ymd triplet dates and hms triplet times into adas, and sort by ada.
{0: 0.0011114386869002235,
1: -4.155124400404918,
2: -2.9710873225303756,
3: -4.635570414590801,
4: 5.116176940364264}
// https://observablehq.com/@mcmcclur/plot-for-mathematicians
{
let w = 800;
let h = 0.625 * w;
let samples = build_samples(
getEot, -366, 365, { N: 150 }
);
let plot = Plot.plot({
marginLeft: 60,
width: w,
height: h,
style: "font-size:19;",
y: {label: "milliday eot(toy)"},
x: {label: "day of year"},
marks: [
Plot.line(samples, {
strokeWidth: 4,
stroke: "steelblue",
tip: true
}),
Plot.ruleX([-366]),
Plot.ruleY([0]),
Plot.axisX({ y: 0 }),
Plot.axisY({ x: -366 })
]
});
return plot;
}The line📈chart above uses md to display eot(toy) values as integers. There is little difference between mst and ast around Days 45, 103, 184, and 299. The difference ranges from about -9.8 on Day 244 to around 11.4 md on Day 350. We can calculate mst with just a tod and a longitude and we will be off by at most about a centiday compared to ast.
Apart from turning a mst into an ast, we can also use eot to more accurately estimate the tods of solar noon, sunrise, and sunset. The equation below creates a solar noon tod measured in days by adding 9.55 to a longitude measured in λ, subtracting a tzo and a eot(toy) value that are both measured in days, and keeping only the decimal part of the result.
\[\text{solarnoon} = (9.55 + \text{tzo} - \lambda - \text{eot(toy)) mod 1}\]
Cambridge and Cambridge
To compare the solar noon tod in two cities, we can plug in the longitude of each city into the equation above. If all the variables in the equation other than longitude are set to zero, the result is almost five decidays for Cambridge, England in the United Kingdom and nearly seven decidays for Cambridge, Massachusetts in the United States.
\[4.99635 = (9.55 - 0.050365) \text{ mod 1} \times 10\]
\[6.97516 = (9.55 - 0.852484) \text{ mod 1} \times 10\]
The two homonymous cities are about two dλ apart and thus will always differ by around two decidays in solar time, regardless of what time zone we use as our frame of reference. England is in Zone 0 and Massachusetts is in Zone 8. If we change the tzo from zero to eight decidays, the solar noon tod for each city will be two decidays earlier.
\[2.99635 = (9.55 + 0.8 - 0.050365) \text{ mod 1} \times 10\]
\[4.97516 = (9.55 + 0.8 - 0.852484) \text{ mod 1} \times 10\]
Full day arc
The path in the sky that the Sun appears to follow from a sunrise to a sunset is a “day arc” (da). Solar noon, the midpoint of a da, is halfway between sunrise and sunset. On each side of solar noon is a “half day arc” (da/2). The sum of a solar noon tod and a da/2 is a sunset tod and the difference between a solar noon tod and a da/2 is a sunrise tod.
\[\begin{split} \text{sunset} & = \text{solarnoon} + \frac{\text{da}}{2} && = \text{sunrise} + \text{da} \\ \text{sunrise} & = \text{solarnoon} - \frac{\text{da}}{2} && = \text{sunset} - \text{da} \end{split}\]
Full night arc
The Sun continues its path after it disappears below the horizon, moving from sunset to sunrise along a “night arc” (na). Like conjugate angles, a da and a na form a full circle that represents one day. Likewise, a da/2 and a “half night arc” (na/2) are like two supplementary angles that form a semicircle from solar noon to the na midpoint: solar midnight.
\[\text{solarmidnight} = \text{solarnoon} + \frac{\text{da}}{2} + \frac{\text{na}}{2} = \text{sunset} + \frac{\text{na}}{2}\]
The range input below controls the yellow da and the blue na in the diagram beneath it. At the Equator, the sunrise always rises about a quarter turn from North and sets around three quarter turns from North, resulting in a da of approximately half a day: 75% – 25% = 50%. If da is zero, a polar night occurs. A day without a na is called a polar day.
{
const svg = d3.create("svg")
.attr("width", width)
.attr("height", daheight);
svg.append("circle")
.attr("stroke", window.darkmode ? "white" : "black")
.attr("fill", "none")
.attr("r", daradius)
.attr("transform", `translate(${dacenter})`);
const tick = svg.selectAll(".tick")
.data([0, 45].map(n => d3.range(n, 360, 90)).flat().sort((a, b) => a - b))
.enter().append("g")
.attr("class", "tick");
tick.append("polyline")
.attr("points", d => [-3, 3].map(n => geometric.pointTranslate(dacenter, -d, daradius + n)))
.attr("stroke", window.darkmode ? "white" : "black");
tick.append("text")
.attr("dy", d => d === 0 || d === 180 ? 4 : d > 0 && d < 180 ? 0 : 8)
.attr("text-anchor", d => d < 90 || d > 270 ? "start" : d > 90 && d < 270 ? "end" : "middle")
.attr("transform", d => `translate(${geometric.pointTranslate(dacenter, -d, daradius + (d === 270 ? 15 : d === 315 ? 14 : d === 225 ? 13 : 12))})`)
.attr("font-family", "sans-serif")
.attr("font-size", 16)
.attr("fill", window.darkmode ? "white" : "black")
.text(d => `${(1000-((d+270)%360)/.36)%1000}`);
svg.append("polyline")
.attr("stroke-width", 2)
.attr("stroke", window.darkmode ? "violet" : "darkviolet")
.attr("points", [dacenter, [daend[0], dacenter[1]]]);
svg.append("polyline")
.attr("stroke-width", 2)
.attr("stroke", window.darkmode ? "red" : "darkred")
.attr("points", [[daend[0], dacenter[1]], daend]);
svg.append("polyline")
.attr("stroke-width", 2)
.attr("stroke", window.darkmode ? "slateblue" : "darkslateblue")
.attr("points", [[dacenter, daend]]);
svg.append("polyline")
.attr("stroke-width", 2)
.attr("stroke", window.darkmode ? "violet" : "darkviolet")
.attr("points", [dacenter, [daend1[0], dacenter[1]]]);
svg.append("polyline")
.attr("stroke-width", 2)
.attr("stroke", window.darkmode ? "red" : "darkred")
.attr("points", [[daend1[0], dacenter[1]], [daend1[0], daend1[1]]]);
svg.append("polyline")
.attr("stroke-width", 2)
.attr("stroke", window.darkmode ? "slateblue" : "darkslateblue")
.attr("points", [[dacenter, [daend1[0], daend1[1]]]]);
svg.append("path")
.attr("transform", `translate(${dacenter})`)
.attr("d", d3.arc()({
innerRadius: 105,
outerRadius: 110,
startAngle: Math.PI - dayArcInput / 1000 * Math.PI,
endAngle: Math.PI + dayArcInput / 1000 * Math.PI
}))
.attr("stroke", window.darkmode ? "white" : "black")
.attr("fill", "gold");
svg.append("path")
.attr("transform", `translate(${dacenter})`)
.attr("d", d3.arc()({
innerRadius: 105,
outerRadius: 110,
startAngle: Math.PI - dayArcInput / 1000 * Math.PI,
endAngle: dayArcInput / 1000 * Math.PI - Math.PI
}))
.attr("stroke", window.darkmode ? "white" : "black")
.attr("fill", "blue");
return svg.node();
}The diagram above can represent both a clock and a compass. In the Northern Hemisphere, solar time is essentially the same as the “solar azimuth angle” (saa). In the Southern Hemisphere, we would need to flip the diagram upside down for it to match the saa. The bar chart below compares the populations of the Northern and Southern Hemispheres.
// https://observablehq.com/@mattdzugan/population-by-time-zone-creating-a-dataset
viewof sortParamsHemi = Inputs.form([
Inputs.toggle({ label: "Sort ascending" }),
Inputs.toggle({ label: "Sort by offset" }),
])Plot.plot({
width: Math.min(width, 1000),
height: 100 + Math.min(width / 25, 20),
marginBottom: 50,
style: `overflow: visible;font-size:${15 + Math.min(width / 90, 11)}px;`,
color: {scheme: "RdBu", className: "barPlotLegend"},
marginLeft: 90 + Math.min(width / 100, 20),
y: { label: null },
x: { grid: true, label: "Population (billions)", labelOffset: 40 + Math.min(width / 90, 22), transform: d => d},
marks: [
Plot.barX(sortedPopHemi, {x: "Population", y: "Hemisphere", fill: "Offset", stroke: "black", tip: true }),
]
})Anyone can measure the saa during the daytime by pointing a compass at the point on the horizon below the Sun. In the Northern Hemisphere, we can use this method to approximate solar time. In the Southern Hemisphere, we can obtain solar time by measuring the saa in turns and then subtracting our measurement from one and a half turns:
\[\text{solartime} \approx \begin{cases}\text{saa}&{\text{if } \phi \geq 0;}\\(1.5 - \text{saa}) \text{ mod } 1&{\text{otherwise.}}\end{cases}\]
The clockwise path that the Sun follows in the Northern Hemisphere is ingrained in the Belarussian, Polish, or Ukrainian languages. These three Slavic languages each have one word for north or midnight and another word for south or noon. In Dec, zero represents both north and midnight, while both south and noon can be expressed as a half turn.
| Turns | Direction | Time | Belarussian | Polish | Ukrainian |
|---|---|---|---|---|---|
| 0 | north | midnight | поўнач | północ | північ |
| 0.5 | south | noon | поўдзень | południe | південь |
If we only want to know how long the Sun will shine on a given day, we can use the top equation below to obtain a da. Alternatively, if we are interested in finding out when the Sun will rise or set on a given day, we will need to calculate a da/2 using the bottom equation below and then combine it with a solar noon tod to get a sunrise or sunset tod.
\[\text{da} = \frac{\arccos\left({\Large\frac{\text{costau(0.252314)} - \text{sintau($\phi$)} \times \text{sintau(sda)}}{\text{costau($\phi$)} \times \text{costau(sda)}}}\right)}{\pi}\]
\[\frac{\text{da}}{2} = \frac{\arccos\left({\Large\frac{\text{costau(0.252314)} - \text{sintau($\phi$)} \times \text{sintau(sda)}}{\text{costau($\phi$)} \times \text{costau(sda)}}}\right)}{\tau}\]
Solar declination angle
The da and da/2 equations above require a latitude and a “solar declination angle” (sda). Dec measures latitude in turns called meridians (φ) or turn submultiples such as millimeridians (mφ). For simplicity, we can fit our eot model to sda data instead of fitting the needlessly complex sda model provided by the NOAA General Solar Position Calculations:
\[\begin{split} \text{sda(toy)} & = \beta_0 \\ & + \beta_1 \times \text{costau(toy)} \\ & + \beta_2 \times \text{sintau(toy)} \\ & + \beta_3 \times \text{costau(2} \times \text{toy)} \\ & + \beta_4 \times \text{sintau(2} \times \text{toy)} \\ & + \beta_5 \times \text{costau(3} \times \text{toy)} \\ & + \beta_6 \times \text{sintau(3} \times \text{toy)} \\ \end{split}\]
The top range input below picks the doy that will become the toy in our fitted sda model. The other two range inputs below chose the geographic coordinates that we need to find the sunrise and sunset tods. While the sunrise and sunset tods depend on both geographic coordinates, the da varies only by latitude and not by longitude: – = .
The equation controlled by range inputs above can be summarized as sunset – sunrise = da and can be rearranged into the sunrise equation: sunrise = sunset – da. If we plug the current tod in place of the sunrise tod in the sunrise equation without changing the sunset tod, the da will be replaced with the time until or since sunset: = .
The latest equation above is an example of a Dec span. From left to right, this span consists of a minuend, an equals sign (=), subtrahend, a plus (+) or a minus (–) sign, and a difference. A minus sign indicates the difference is the time remaining until sunset, whereas a plus sign means that the difference is the time that has passed since sunset.
The difference shows the remaining time as a negative number if the minuend is less than the substrahend or displays the elapsed time as a positive number if the minuend is greater than the subtrahend. In this way, the difference can act like a “T minus countdown” that becomes a “T plus countup” after an event like a rocket🚀launch.
\[\begin{cases}\text{difference}<0&{\text{if } \text{minuend} < \text{subtrahend};}\\\text{difference}=0&{\text{if minuend} = \text{subtrahend};}\\\text{difference}>0&{\text{if minuend}>\text{subtrahend.}}\end{cases}\]
When the minuend is the current time and the subtrahend is the timestamp of an event which occurred in the past, the difference is the time elapsed since that event. A Dec timestamp consists of a year, a day, . can be an eda, a “epochal year aggregate” (eya), or a snap🫰. Edas and eyas are the time The typical snap format is year+ada-tzo. The snap +-0 represents the b when this webpage loaded b ago.
We can omit the year from a snap if we replace the ada with a subtrahend and a difference or just a difference. If needed, we can obtain a snap from a difference using the equations below. First, we subtract the difference from the current eda to obtain the “eda difference difference” (edd). Then, we use the edd to get the “cycle of era” (coe), “day of cycle” (doc), “year of cycle” (yoc), and then finally the year and ada.
\[\text{edd} = \text{eda} - \text{difference}\]
\[\text{coe} = \Biggl \lfloor \frac{\begin{cases}\text{edd}&{\text{if } \text{edd} \geq 0;}\\\text{edd}-146096&{\text{otherwise.}}\end{cases}}{146097} \Biggr \rfloor\]
\[\text{doc} = \text{edd} - \text{coe} \times 146097\]
\[\text{yoc} = \biggl \lfloor \frac{\text{doc} - \lfloor \frac{\text{doc}}{1460} \rfloor + \lfloor \frac{\text{doc}}{36524} \rfloor - \lfloor \frac{\text{doc}}{146096} \rfloor}{365} \biggr \rfloor\]
\[\text{year} = \text{yoc} + \text{coe} \times 400\]
\[\text{ada} = \text{doc} - \text{yoc} \times 365 - \lfloor \frac{\text{yoc}}{4} \rfloor + \lfloor \frac{\text{yoc}}{100} \rfloor\]
In the equations above, eda is “epochal day aggregate”, coe is “cycle of era”, doc is “day of cycle”, and yoc is “year of cycle”.
is “epochal day aggregate” (eda), If a subtrahend does not include a and a difference We do not need to include a with , because can also omit the . The current Zone 0 yoe+ada timestamp is .
omit the yoe by passing the subtrahend, difference, and current “epochal day aggregate” (eda) to the
We can omit the tzo if the timestamp is based on Zone 0. The time since this webpage was loaded is current - load = diff. If needed, calculate the year by subtracting the ada and Δ from the current eda and passing the result to the equations below.
To make it easier to compare timestamps, we should agree to always set the tzo to zero. If we use a non-zero tzo, we should include it in between the event time and the elapsed time.
Let’s say that you live in Zone 0 and there are four tods that are vital to your typical daily rhythm: you start work at 375 md, take a lunch break from 525 to 575 md, and finish work at 725 md. After one event passes, you can begin tracking the time until the next one and thus cycle through all four over the course of each day: current = 375 diff = 525 diff = 575 diff = 725 diff.
Tzo range input (include tzo only if not zero)
Another use case for minuend expansion is travel. If the current tod is the minuend and your estimated arrival time is the subtrahend, then travel time remaining will be the difference. When traveling, we can replace time with distance so that distance traveled so far is the minuend, the total distance is the subtrahend, and the distance remaining is the difference. Apart from travel, measuring distance in addition to time can be useful for tracking exercise such as running, bicycling, or swimming.
To measure distances, Dec uses taurs (c) or zems (z), with or without metric prefixes, depending on the order of magnitude of the distance being measured. One c is close to the circumference of the earth and is equal to 105 kilozem (kz) or 4 × 105 kilometers (km). The distance between the Earth and the Moon ranges from 9.065 to 10.135 c. For larger distances, we can use astronomical units (au), light years (ly), or multiples of c such as kilotaurs (kc) and gigataurs (gc).
au = 3740 c = 3.74 kc ly = 236525000 c = 236525 kc = 2.36525 gc
Speed is distance over time. Dec measures speed in omegar (v). The speed of the rotation of the Earth is close to one v and is equal to one c per day or one kz per b. The average speed of the orbit of the Moon around the Earth is about 2.2 v and the mean speed of the Earth orbit around the Sun is approximately 64 v. For higher speeds, we can use percent (%) or permille (‰) light speed (ls) and multiples of v like kv.
%ls = 10‰ls = 647552 v = 647.552 kv
For distances that we can measure on Earth, we may want to use submultiples of c such as mc or nc. One mc is 100 kz or 40 km and one nc is 1 decizem (dz) or 4 centimeters (cm). Similarly, we can use submultiples of v for speeds on Earth. One milliomegar (mv) is one mc per day, one kz per centiday (cd), or one z per b and is very close to one mile per hour. An airplane flying at an average speed of 500 mv will travel the 12500 kz from Cambridge MA to Cambridge UK in a quarter day: 12500 kz / 500 mv = 25 cd.
Around the world in 80 days is a book published by Jules Verne in 1872. The average speed of a trip around the circumference of the earth in 80 days would be about 1 / 80 v or 12.5 mv, which is a typical speed for a bicycle. If we flew in a “pedal powered airship” (https://en.wikipedia.org/wiki/Airship#:~:text=pedal%20powered%20airship) from Cambridge MA to Cambridge UK at an average speed of 12.5 mv, we could complete the journey in ten days: 12500 kz / 12.5 mv = 1000 cd.
At sunrise, the da is the amount of time until sunset. At solar noon, the time remaining before sunset will be da/2. Throughout the day, the current tod is equal to the sum of sunset tod and the difference between the current and sunset tods: – = .
Like a countdown sequence, we can keep track of the time relative to sunset throughout the day. Instead of the current tod, we can display the , the and the current tod is equal to the difference between the sunset tod and the da and the current tod is equal to the difference between the sunset tod and the da/2. The Given a sunset tod of We can replace the current tod with an expression that provides the time relative to a given tod.
can keep track of the remaining time until sunset by minuend The tod of sunrise is found by subtracting half the da from solar noon tod. The tod of sunset is found by adding half the da to solar noon tod.
Sunrise and sunset
Solar hour angle
To find the UTC tzo of a given longitude and latitude, we could use an application programming interface (API) or a database. If we only have longitude, we need to first round degrees to zero or the nearest multiple of fifteen for whole hour tzos, 7.5 for half hour tzos, or 3.75 for quarter hour tzos and then divide by fifteen to convert degrees to hours.
Next
The next article in the Dec section of my site shows how we can combine Dec dates and times into Dec snaps🫰, which are analogous to the combined date and time representations in the ISO 8601 international standard for dates and times. The final article in the Dec section demonstrates how Dec dates, times, and snaps🫰can be paired up to express time intervals called Dec spans🌈.
%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '32px'}}}%%
flowchart LR
A[Dec]-->B[date]-->C[time]-->D[snap]-->E[span]
click A "/dec"
click B "/dec/date"
click C "/dec/time"
click D "/dec/snap"
click E "/dec/span"
Cite
Please spread the good word about Dec using the citation information at the bottom of this article. You may also want to cite the Observable notebooks that I adapted into the clock🕓, bar📊chart, map🗺️, and daylight☀️plot visualizations in this article or the 2014 blog post which proposed a system of 20 decimal time zones, each 5 centidays wide, based on the Greenwich Meridian:
- Pearson, Tom. 2013+124. “Simple D3 clock.” +. https://observablehq.com/@d3/simple-clock.
- Heyen, Frank. 2021+246. “BarChart Clock.” +. https://observablehq.com/@fheyen/barchart-clock.
- Johnson, Ian. 2021+090. “Draggable World Map Coordinates Input.” +. https://observablehq.com/@enjalot/draggable-world-map-coordinates-input.
- Bridges, Dan. 2021+311. “Visualizing Seasonal Daylight.” +. https://observablehq.com/@dbridges/visualizing-seasonal-daylight.
- Clements, John. 2014+091, “Decimal Time Zones.” +. https://www.brinckerhoff.org/blog/2014/05/31/decimal-time-zones.
function unix2dote(unix, zone, offset = 719468) {
return [(unix ?? Date.now()) / 86400000 + (
zone = zone ?? -Math.round(
(new Date).getTimezoneOffset() / 144)
) / 10 + offset, zone]
}
function unix2dote1(unix, zone, offset = 719468) {
return [
(unix ?? Date.now()) / 86400000
+ (zone = zone ?? (10 - Math.round(
(new Date).getTimezoneOffset() / 144)) % 10
) / 10 + offset, zone]
}
function dote2date(dote, zone = 0) {
const cote = Math.floor((
dote >= 0 ? dote
: dote - 146096
) / 146097),
dotc = dote - cote * 146097,
yotc = Math.floor((dotc
- Math.floor(dotc / 1460)
+ Math.floor(dotc / 36524)
- Math.floor(dotc / 146096)
) / 365);
return [
yotc + cote * 400,
dotc - (yotc * 365
+ Math.floor(yotc / 4)
- Math.floor(yotc / 100)
), zone]}
function getEot(day) {
const gamma = 2 * Math.PI * day / 365
return (
0.0011114386869002235 +
-4.155124400404918 * Math.cos(gamma) +
-2.9710873225303756 * Math.sin(gamma) +
-4.635570414590801 * Math.cos(2 * gamma) +
5.116176940364264 * Math.sin(2 * gamma)
)
}
function getSda(day) {
const gamma = 2 * Math.PI * day / 365
return (
0.001091553079960761 +
-0.023117708562197206 * Math.cos(gamma) +
0.060320423699681706 * Math.sin(gamma) +
0.0006152631125526081 * Math.cos(2 * gamma) +
0.0008981745798778195 * Math.sin(2 * gamma)
)
}
function getSha(latitude, solarDeclinationAngle, solarZenithAngle = 0.252314) {
const ratio = (
costau(.252314) - sintau(latitude) * sintau(solarDeclinationAngle)) / (
costau(latitude) * costau(solarDeclinationAngle))
return ratio >= 1 ? 0 : ratio <= -1 ? Math.PI : Math.acos(ratio)
}
cospi = require( 'https://cdn.jsdelivr.net/gh/stdlib-js/math-base-special-cospi@umd/browser.js' )
sinpi = require( 'https://cdn.jsdelivr.net/gh/stdlib-js/math-base-special-sinpi@umd/browser.js' )
function sintau(x) {
return sinpi(2 * x);
}
function costau(x) {
return cospi(2 * x);
}
shaRadiMapPlot = getSha(latTurnMapPlot, getSda(selAda))
shaHalfMapPlot = shaRadiMapPlot / Math.PI
shaTurnMapPlot = shaHalfMapPlot / 2
shaDegrMapPlot = shaHalfMapPlot * 180
eotTurnMapPlot = getEot(selAda) / 1000
lonTurnMapPlot = long2turn(location[0], 0)
latTurnMapPlot = lati2turn(location[1], 0)
astDecMapPlot = (0.95 + zeroTime + lonTurnMapPlot + eotTurnMapPlot) % 1 * 10
astDegMapPlot = astDecMapPlot * 36
astDegMapHalf = (astDecMapPlot * 36 + 180) % 360
shaDoyInput = getSha(latInput / 1000, getSda(doyInput))
daytimeDuration = shaDoyInput / Math.PI
noonDiff = daytimeDuration / 2
selectedSolarNoon = (9.55 - lonInput / 100 % 1 / 10 - getEot(doyInput) / 1000) % 1
selectedTimeZone = Math.floor(lonInput / 100)
selectedCurrent = (zeroTime + selectedTimeZone / 10) % 1
selectedSunrise = parseFloat((selectedSolarNoon - noonDiff).toFixed(5))
selectedSunset = selectedSolarNoon + noonDiff
selectedDifference = selectedCurrent - selectedSunset
selectedDuration = parseFloat((selectedSunset - selectedSunrise).toFixed(5))
dz = unix2dote(now)
decYear = ydz[0].toString().padStart(4, "0")
zeroDote = unix2dote(now, 0)[0]
zeroTime = zeroDote % 1
zeroDate = dote2date(zeroDote)
zeroYear = zeroDate[0]
zeroAda = zeroDate[1]
zeroDoy = Math.floor(zeroAda)
zeroIsLeap = isLeapYear(zeroYear)
zeroAdaHsl = textcolor(zeroAda.toFixed(5), d3.color(piecewiseColor(zeroAda / (365 + zeroIsLeap))).formatHex())
zeroAdaHsl1 = textcolor(zeroAda.toFixed(5), d3.color(piecewiseColor(zeroAda / (365 + zeroIsLeap))).formatHex())
zeroTimeHsl0 = textcolor(zeroTime.toFixed(5).slice(1), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroTimeHsl1 = textcolor(zeroTime.toFixed(5).slice(1), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroTimeHsl2 = textcolor((zeroTime * 10).toFixed(4), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroTimeHsl3 = textcolor((zeroTime * 10).toFixed(4), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroTimeHsl4 = textcolor((zeroTime * 10).toFixed(4), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroTimeHsl5 = textcolor((zeroTime * 10).toFixed(5), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroDotyHsl = textcolor(zeroDoy.toString().padStart(3, "0"), d3.color(piecewiseColor(zeroDoy / (365 + zeroIsLeap))).formatHex())
zeroDoteHsl = textcolor(zeroDote.toFixed(5), d3.color(piecewiseColor(zeroDote % 1)).formatHex())
zeroYearHsl = textcolor(zeroYear, d3.color(piecewiseColor(zeroYear % 1000 / 1000)).formatHex())
browserDote = unix2dote(now)
browserTime = browserDote[0] % 1 * 10
browserZone = browserDote[1]
browserSign = browserZone > 0 ? "-" : "+"
zone0time = (browserTime - browserZone + 10) % 10
hours = browserTime * 2.4
minutes = hours % 1 * 60
seconds = minutes % 1 * 60
selectedDote = unix2dote(now, long2zone(location[0]))
selectedExact = selectedDote[0] % 1
selectedExactN = (1 - selectedExact) % 1
selectedZone = selectedDote[1]
ydz = dote2date(...selectedDote)
decDate = Math.floor(ydz[1])
decTime = (selectedExact * 10).toFixed(4)
decTimeN = (selectedExactN * 10).toFixed(4)
barDD = decTime[0]
barDDN = decTimeN[0]
barMils = decTime.slice(2, 4)
barMilsN = decTimeN.slice(2, 4)
barBeats = decTime.slice(4, 6)
barBeatsN = decTimeN.slice(4, 6)
function lati2turn(degrees = -180, e = 3) {
const base = 10 ** e, limit = base / 4;
return Math.max(-limit, Math.min(limit, (degrees / 360) * base));
}
function turn2lati(turns = -500, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return Math.max(-90, Math.min(90, turns * 360 / 10**e))
}
graticule = d3.geoGraticule().stepMinor([36,36]).stepMajor([36,36])()
graticule.coordinates = graticule.coordinates.map(
i => i.map(j => j.map((k, index, arr) => i.length === 3 && index === 0 ? k - 18 : k))
)function turn2degr(turns = -500, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return turns % 10**e * 360 / 10**e
}
function turn2long(turns = -500, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return turns % 10**e * (360 / 10**e) - 18
}
function long2turn(degrees = -180, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return (((degrees %= 360) < 0 ? degrees + 360 : degrees) + 18) / (360 / 10**e) % 10**e;
}
// https://github.com/topojson/world-atlas
world = d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
// countries = topojson.feature(world, world.objects.countries)
topojson = require("topojson-client@3")
function long2zone(degrees = -180) {
return Math.floor(long2turn(degrees, 1));
}
function year2leap(year = 1970) {
return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
}
// https://observablehq.com/@dbridges/visualizing-seasonal-daylight
solarSystem = (root, vizwidth, location, date, hour, darkmode) => {
const earthRadius = 0.065 * vizwidth;
// const sunRadius = 0.015 * vizwidth;
const solarSystemRadius = vizwidth / 2 - (vizwidth < 500 ? 4 : 20);
const stretch = 0.06;
const solarAngle = getSolarAngle(date);
const solarAngleDeg = (solarAngle * 180) / Math.PI;
const x = solarSystemRadius * Math.sin(solarAngle);
const y = stretch * solarSystemRadius * Math.cos(solarAngle);
const spin = 180 + -location[0] + solarAngleDeg + 360 * ((hour + 12) / 24);
const earthGeo = { type: "Sphere" };
const projection = d3
.geoOrthographic()
.fitWidth(earthRadius * 2, earthGeo)
.rotate([spin, 0, 23.5])
.translate([0, 0]);
const staticProjection = d3
.geoOrthographic()
.fitWidth(earthRadius * 2, earthGeo)
.rotate([solarAngleDeg - 90, 0])
.translate([0, 0]);
const path = d3.geoPath(projection).pointRadius(1.5);
const staticPath = d3.geoPath(staticProjection);
const oceanColor = darkmode ? "#007FFF" : mapcolors.ocean;
const landColor = darkmode ? "#0808" : mapcolors.land;
const borderColor = darkmode ? "#eee" : "#333";
const solarSystem = root
.append("g")
.attr("transform", `translate(${vizwidth / 2})`);
// solarSystem.append("circle").attr("r", sunRadius).attr("fill", colors.sun);
/* Draw orbit */
solarSystem
.append("ellipse")
.attr("rx", solarSystemRadius)
.attr("ry", stretch * solarSystemRadius)
.attr("fill", "none")
.attr("stroke-width", `${width < 300 ? .3 : width < 400 ? .4 : width < 500 ? .5 : width < 600 ? .6 : width < 700 ? .7 : width < 800 ? .8 : width < 900 ? .9 : width < 1000 ? 1 : width < 1100 ? 1.1 : width < 1200 ? 1.2 : width < 1300 ? 1.3 : width < 1400 ? 1.4 : width < 1500 ? 1.5 : width < 1600 ? 1.6 : width < 1700 ? 1.7 : width < 1750 ? 1.75 : width < 1800 ? 1.8 : width < 1900 ? 1.9 : width < 2000 ? 2 : width < 2100 ? 2.1 : width < 2200 ? 2.2 : width < 2300 ? 2.3 : width < 2400 ? 2.4 : width < 2500 ? 2.5 : width < 2600 ? 2.6 : width < 2700 ? 2.7 : width < 2800 ? 2.8 : width < 2900 ? 2.9 : width < 3000 ? 3 : width < 3100 ? 3.1 : width < 3200 ? 3.2 : width < 3300 ? 3.3 : width < 3400 ? 3.4 : width < 3500 ? 3.5 : width < 3600 ? 3.6 : width < 3700 ? 3.7 : width < 3800 ? 3.8 : width < 3900 ? 3.9 : width < 4000 ? 4 : width < 4100 ? 4.1 : width < 4200 ? 4.2 : width < 4300 ? 4.3 : width < 4400 ? 4.4 : width < 4500 ? 4.5 : width < 4600 ? 4.6 : width < 4700 ? 4.7 : width < 4800 ? 4.8 : width < 4900 ? 4.9 : width < 5000 ? 5 : width < 5100 ? 5.1 : width < 5200 ? 5.2 : width < 5300 ? 5.3 : width < 5400 ? 5.4 : width < 5500 ? 5.5 : width < 5600 ? 5.6 : width < 5700 ? 5.7 : width < 5800 ? 5.8 : majwid / 1000}`)
.attr("stroke", "black");
/* Draw month ticks */
d3.range(12).map((m) => {
const d = new Date(date.getFullYear(), m, 1);
const angle = getSolarAngle(d);
const llmultiplier = [5, 11].includes(m) ? 3 : [4, 10].includes(m) ? 1.75 : [0, 6].includes(m) ? 1.5 : [3, 9].includes(m) ? 1.25 : 1;
const linelength = llmultiplier * (width < 300 ? 13 : width < 400 ? 14 : width < 500 ? 15 : width < 600 ? 16 : width < 700 ? 17 : width < 800 ? 18 : width < 900 ? 19 : width < 1000 ? 20 : width < 1100 ? 21 : width < 1200 ? 22 : width < 1300 ? 23 : width < 1400 ? 24 : width < 1500 ? 25 : width < 1600 ? 26 : width < 1700 ? 27 : width < 1750 ? 27.5 : width < 1800 ? 28 : width < 1900 ? 39 : width < 2000 ? 50 : width < 2100 ? 61 : width < 2200 ? 62 : width < 2300 ? 63 : width < 2400 ? 64 : width < 2500 ? 65 : width < 2600 ? 66 : width < 2700 ? 67 : width < 2800 ? 68 : width < 2900 ? 69 : width < 3000 ? 70 : width < 3100 ? 71 : width < 3200 ? 72 : width < 3300 ? 73 : width < 3400 ? 74 : width < 3500 ? 75 : width < 3600 ? 76 : width < 3700 ? 77 : width < 3800 ? 78 : width < 3900 ? 79 : width < 4000 ? 80 : width < 4100 ? 81 : width < 4200 ? 82 : width < 4300 ? 83 : width < 4400 ? 84 : width < 4500 ? 85 : width < 4600 ? 86 : width < 4700 ? 87 : width < 4800 ? 88 : width < 4900 ? 89 : width < 5000 ? 90 : width < 5100 ? 91 : width < 5200 ? 92 : width < 5300 ? 93 : width < 5400 ? 94 : width < 5500 ? 95 : width < 5600 ? 96 : width < 5700 ? 97 : width < 5800 ? 98 : majwid / 59);
solarSystem
.append("line")
.attr("x1", (solarSystemRadius + linelength) * Math.sin(angle))
.attr("y1", (solarSystemRadius + linelength) * stretch * Math.cos(angle))
.attr("x2", (solarSystemRadius - linelength) * Math.sin(angle))
.attr("y2", (solarSystemRadius - linelength) * stretch * Math.cos(angle))
.attr("stroke-width", `${width < 300 ? .6 : width < 400 ? .8 : width < 500 ? 1 : width < 600 ? 1.2 : width < 700 ? 1.4 : width < 800 ? 1.6 : width < 900 ? 1.8 : width < 1000 ? 2 : width < 1100 ? 2.2 : width < 1200 ? 2.4 : width < 1300 ? 2.6 : width < 1400 ? 2.8 : width < 1500 ? 3 : width < 1600 ? 3.2 : width < 1700 ? 3.2 : width < 1750 ? 3.3 : width < 1800 ? 3.4 : width < 1900 ? 3.6 : width < 2000 ? 4 : width < 2100 ? 4.2 : width < 2200 ? 4.4 : width < 2300 ? 4.6 : width < 2400 ? 4.8 : width < 2500 ? 5 : width < 2600 ? 5.2 : width < 2700 ? 5.4 : width < 2800 ? 5.6 : width < 2900 ? 5.8 : width < 3000 ? 6 : width < 3100 ? 6.2 : width < 3200 ? 6.4 : width < 3300 ? 6.6 : width < 3400 ? 6.8 : width < 3500 ? 7 : width < 3600 ? 7.2 : width < 3700 ? 7.4 : width < 3800 ? 7.6 : width < 3900 ? 7.8 : width < 4000 ? 8 : width < 4100 ? 8.2 : width < 4200 ? 8.4 : width < 4300 ? 8.6 : width < 4400 ? 8.8 : width < 4500 ? 9 : width < 4600 ? 9.2 : width < 4700 ? 9.4 : width < 4800 ? 9.6 : width < 4900 ? 9.8 : width < 5000 ? 9 : width < 5100 ? 9.2 : width < 5200 ? 9.4 : width < 5300 ? 9.6 : width < 5400 ? 9.8 : width < 5500 ? 10 : width < 5600 ? 10.2 : width < 5700 ? 10.4 : width < 5800 ? 10.6 : majwid / 547}`)
.attr("stroke", "black");
const startMonthAngle = getSolarAngle(new Date(date.getFullYear(), m, 1));
solarSystem
.append("text")
.text(date2doty(d))
.attr("x", (solarSystemRadius + 18 - majwid / 50) * Math.sin(startMonthAngle) * (width < 350 ? 1.05 : width < 600 ? 1.08 : width < 700 ? 1.1 : width < 769 ? 1.11 : width < 1000 ? 1.1 : width < 1700 ? 1.12 : width < 1800 ? 1.12 : width < 1900 ? 1.13 : width < 2000 ? 1.142 : width < 3000 ? 1.15 : width < 4000 ? 1.16 : width < 5000 ? 1.16 : 1.16))
.attr(
"y",
(solarSystemRadius + 2 - majwid / 2.8) * 7 * stretch * Math.cos(startMonthAngle) + Math.sign(Math.cos(startMonthAngle)) * (width < 300 ? 8 : width < 400 ? 8 : width < 450 ? 8 : width < 500 ? 7 : width < 600 ? 8 : width < 700 ? 8 : width < 769 ? 8 : width < 800 ? 13.5 : width < 850 ? 13.75 : width < 900 ? 14 : width < 950 ? 15 : width < 1000 ? 16 : width < 1100 ? 20 : width < 1200 ? 20 : width < 1300 ? 20 : width < 1400 ? 20 : 12)
)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", majwid / 1000 * (width < 350 ? 30 : width < 400 ? 30 : width < 450 ? 30 : width < 500 ? 30 : width < 550 ? 30 : width < 600 ? 30 : width < 650 ? 30 : width < 675 ? 30 : width < 700 ? 30 : width < 750 ? 30 : width < 760 ? 30 : width < 769 ? 30 : width < 770 ? 30 : width < 780 ? 30 : width < 800 ? 30 : width < 900 ? 30 : width < 950 ? 30 : width < 975 ? 30 : width < 1000 ? 30 : width < 1100 ? 30 : width < 1200 ? 30 : width < 1250 ? 30 : width < 1300 ? 30 : width < 1400 ? 30 : width < 1440 ? 30 : width < 1450 ? 30 : width < 1480 ? 30 : width < 1500 ? 30 : width < 1520 ? 30 : width < 1540 ? 30 : width < 1560 ? 30 : width < 1600 ? 30 : width < 1620 ? 30 : width < 1640 ? 30 : width < 1660 ? 30 : width < 1680 ? 30 : width < 1700 ? 30 : width < 1720 ? 30 : width < 1780 ? 30 : width < 1820 ? 30 : width < 1880 ? 30 : width < 1920 ? 30 : width < 1970 ? 30 : width < 2000 ? 30 : width < 2100 ? 30 : width < 2200 ? 30 : width < 2300 ? 30 : width < 2400 ? 30 : width < 2500 ? 30 : width < 2600 ? 30 : width < 2700 ? 30 : width < 2800 ? 30 : width < 2900 ? 27 : width < 2950 ? 30 : width < 3000 ? 30 : width < 3100 ? 30 : width < 3200 ? 30 : width < 3250 ? 30 : width < 3300 ? 30 : width < 3400 ? 30 : width < 3450 ? 30 : width < 3500 ? 30 : width < 3550 ? 30 : width < 3600 ? 30 : width < 3700 ? 30 : width < 3750 ? 30 : width < 3800 ? 30 : width < 3850 ? 30 : width < 3900 ? 30 : width < 4000 ? 30 : width < 4100 ? 30 : width < 4200 ? 30 : width < 4250 ? 27 : width < 4300 ? 27 : width < 4325 ? 27 : width < 4350 ? 27 : width < 4400 ? 27 : width < 4500 ? 27 : width < 4550 ? 27 : width < 4600 ? 27 : width < 4650 ? 27 : width < 4700 ? 27 : width < 4800 ? 27 : width < 4850 ? 27 : width < 4900 ? 27 : width < 4950 ? 27 : width < 5000 ? 27 : width < 5050 ? 27 : width < 5100 ? 27 : width < 5150 ? 28 : width < 5250 ? 28 : width < 5300 ? 28 : width < 5325 ? 28 : width < 5325 ? 28 : width < 5350 ? 28 : width < 5450 ? 28 : width < 5500 ? 28 : width < 5550 ? 29 : width < 5600 ? 29 : 30))
.attr("font-family", "sans-serif")
.attr("fill", "black");
});
const earth = solarSystem
.append("g")
.attr("transform", `translate(${x}, ${y})`);
function drawEarth() {
earth.append("line").attr("y1", -1.5 * earthRadius).attr("y2", 1.5 * earthRadius).attr("stroke", "blue").attr("transform", `rotate(-23.5)`).attr("stroke-width", `${width < 500 ? .25 : width < 1000 ? .5 : width < 1500 ? 1 : width < 2000 ? 1.5 : width < 2500 ? 2 : width < 3000 ? 2.2 : width < 3500 ? 2.4 : width < 4000 ? 2.6 : width < 4500 ? 2.8 : width < 5000 ? 3 : width < 5500 ? 3.5 : 4}`);
earth.append("path").attr("d", path(earthGeo)).attr("fill", darkmode ? "#007FFF" : mapcolors.ocean).attr("id", "globeOcean");
earth.append("path").attr("d", path(land)).attr("fill", darkmode ? "#0808" : mapcolors.land).attr("id", "globeLand");
earth.append("path").attr("d", path(countries)).attr("stroke-width", `${width < 500 ? .1 : width < 1000 ? .2 : width < 1500 ? .3 : width < 2000 ? .4 : width < 2500 ? .5 : width < 3000 ? .6 : width < 3500 ? .7 : width < 4000 ? .8 : width < 4500 ? .9 : width < 5000 ? 1 : width < 5500 ? 1.1 : 1.2}`).attr("fill", "none").attr("stroke", darkmode ? "#eee" : "#333").attr("id", "globeBorders");
path.pointRadius(width < 500 ? 2.75 : width < 1000 ? 5.5 : width < 2000 ? 11 : width < 3000 ? 16.5 : width < 4000 ? 22 : width < 5000 ? 27.5 : 33);
earth.append("path").attr("d", path({ type: "Point", coordinates: location })).attr("fill", "none").attr("stroke-width", width < 500 ? .3 : width < 1000 ? .6 : width < 2000 ? 1.2 : width < 3000 ? 1.8 : width < 4000 ? 2.4 : width < 5000 ? 3 : 3.6).attr("stroke", "black");
path.pointRadius(width < 500 ? 2.25 : width < 1000 ? 4.5 : width < 2000 ? 9 : width < 3000 ? 13.5 : width < 4000 ? 18 : width < 5000 ? 22.5 : 27);
earth.append("path").attr("d", path({ type: "Point", coordinates: location })).attr("fill", "none").attr("stroke-width", width < 500 ? 1.125 : width < 1000 ? 2.25 : width < 2000 ? 4.5 : width < 3000 ? 6.75 : width < 4000 ? 9 : width < 5000 ? 11.25 : 13.5).attr("stroke", "red");
}
drawEarth();
}
function greg2doty(month = 1, day = 1) {
return Math.floor(
(153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1
)}
function date2doty(date) {
return greg2doty(date.getMonth() + 1, date.getDate())
}
function date2doty1(date) {
return greg2doty(date.getMonth() + 1, date.getDate())
}
// https://observablehq.com/@dbridges/visualizing-seasonal-daylight
// globe = (root, { vizwidth, location, date, hour }) => {
// const solarAngle = getSolarAngle(date);
// const solarAngleDeg = toDegrees(solarAngle);
// const hourSpin = 360 * ((hour + 12) / 24);
// const spin = (180 + -location[0] + solarAngleDeg + hourSpin);
// const tilt = -15;
// const projection = d3.geoOrthographic()
// .fitWidth(vizwidth, graticule)
// .rotate([spin, tilt, 23.5]);
// const path = d3.geoPath(projection);
// const unClippedProjection = d3.geoOrthographic()
// .clipAngle(null)
// .fitWidth(vizwidth, graticule)
// .rotate([spin, tilt, 23.5]);
// const unClippedPath = d3.geoPath(unClippedProjection);
// const staticProjection = d3.geoOrthographic()
// .fitWidth(vizwidth, graticule)
// .rotate([solarAngleDeg - 90, tilt]);
// const staticPath = d3.geoPath(staticProjection);
// const background = root.append("g");
// const earth = root.append("g").style("opacity", 0.75);
// const foreground = root.append("g");
// earth.append("path")
// .attr("d", path({type: "Sphere"}))
// .attr("fill", mapcolors.ocean)
// .attr("stroke", "#9ecbda");
// earth.append("path")
// .attr("d", path(land))
// .attr("fill", mapcolors.land);
// earth.append("path")
// .attr("d", path(countries))
// .attr("stroke-width", "1")
// .attr("fill", "none")
// .attr("stroke", "#000");
// background.append("path")
// .attr("d", unClippedPath({type: "Point", coordinates: location}))
// .attr("fill", "red");
// const latitudeCoords = (latitude, start, end) => {
// const longitudes = d3.range(start, end, 2).concat(end);
// return longitudes.map(d => [d, latitude]);
// }
// const correctSpin = d3.geoRotation([-hourSpin, 0]);
// const correctTilt = d3.geoRotation([6, 0, 0]);
// /* total angular extent of day/night */
// const dayExtent = 360 * dayLength(date, location[1]) / 24;
// const nightExtent = 360 - dayExtent;
// const dayLine = {
// type: "LineString",
// coordinates: latitudeCoords(location[1],
// location[0] - dayExtent / 2,
// location[0] + dayExtent / 2).map(d => correctSpin(d))
// };
// const nightLine = {
// type: "LineString",
// coordinates: latitudeCoords(location[1],
// location[0] - dayExtent / 2 - nightExtent,
// location[0] - dayExtent / 2).map(d => correctSpin(d))
// };
// background.append("path")
// .attr("d", unClippedPath(dayLine))
// .attr("fill", "none")
// .attr("stroke", mapcolors.day)
// .attr("stroke-width", 3);
// background.append("path")
// .attr("d", unClippedPath(nightLine))
// .attr("fill", "none")
// .attr("stroke", mapcolors.night)
// .attr("stroke-width", 3);
// foreground.append("path")
// .attr("d", path(dayLine))
// .attr("fill", "none")
// .attr("stroke", mapcolors.day)
// .attr("stroke-width", 3);
// foreground.append("path")
// .attr("d", path(nightLine))
// .attr("fill", "none")
// .attr("stroke", mapcolors.night)
// .attr("stroke-width", 3);
// foreground.append("path")
// .attr("d", path({type: "Point", coordinates: location}))
// .attr("stroke-width", .5)
// .attr("stroke", "black")
// .attr("fill", "red");
// const shadowPolygon = [[0, -90], [0, 0], [0, 90], [180, 0], [0, -90]].map(d => correctTilt(d));
// foreground.append("path")
// .attr("d", staticPath({type: "Polygon", coordinates: [shadowPolygon]}))
// .attr("fill", "rgba(0, 0, 0, 0.25)");
// }
dayOfYear = (date) => {
const yearStart = new Date(date.getFullYear(), 0, 1+60);
return Math.floor((date.getTime() - yearStart.getTime())/86400000) + 1
}
// https://observablehq.com/@dbridges/visualizing-seasonal-daylight
daylightPlot = (
root,
{ vizwidth, height, year, latitude, defaultDate, defaultHour }
) => {
const margin = { top: 32, bottom: 32, left: 32, right: 0 };
const chartWidth = vizwidth - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
const yTickValues =
width > 250 ? [3, 6, 9, 12, 15, 18, 21] : width > 100 ? [6, 12, 18] : [12];
const yScale = d3
.scaleLinear()
.domain([0, 24])
.range([margin.left, margin.left + chartWidth])
.clamp(true);
// y-axis scale
const xScale = d3
.scaleTime()
.domain([new Date(year, 0, 61), new Date(year, 11, 91)])
.range([margin.top, margin.top + chartHeight])
.clamp(true);
// y-axis labels
const xAxis = d3
.axisBottom(xScale)
.tickValues(d3.timeMonth.range(new Date(year, 0, 60), new Date(year, 12, 57)))
.tickSize(chartWidth)
.tickFormat(date2doty1);
const yAxis = d3
.axisLeft(yScale)
.tickValues(yTickValues)
.tickSize(chartHeight)
.tickFormat((d) => { return `${d / .024}` });
let date = defaultDate || new Date();
let hour = defaultHour != null ? defaultHour : date.getHours();
const handleMouseMove = (e) => {};
root
.append("rect")
.attr("y", margin.left)
.attr("x", margin.top)
.attr("height", chartWidth)
.attr("width", chartHeight)
.attr("ry", 0.05 * vizwidth)
.attr("fill", mapcolors.night);
root
.append("g")
.attr("transform", `translate(0, ${margin.top})`)
.call(xAxis)
.call((g) => g.select(".domain").remove())
.call((g) => g.selectAll(".tick").attr("color", mapcolors.grid))
.call((g) => g.selectAll(".tick text").attr("font-size", (width < 200 ? .3 : width < 210 ? .34 : width < 220 ? .38 : width < 230 ? .42 : width < 240 ? .46 : width < 250 ? .50 : width < 260 ? .54 : width < 270 ? .58 : width < 280 ? .62 : width < 290 ? .66 : width < 300 ? .70 : width < 310 ? .74 : width < 320 ? .78 : width < 330 ? .82 : width < 340 ? .86 : width < 350 ? .9 : width < 360 ? .92 : width < 370 ? .94 : width < 380 ? .96 : width < 390 ? .98 : width < 400 ? 1 : width < 410 ? 1.02 : width < 420 ? 1.04 : width < 430 ? 1.06 : width < 440 ? 1.08 : width < 450 ? 1.1 : width < 460 ? 1.12 : width < 470 ? 1.14 : width < 480 ? 1.16 : width < 490 ? 1.18 : width < 500 ? 1.2 : width < 550 ? 1.3 : width < 600 ? 1.4 : width < 650 ? 1.6 : width < 700 ? 1.8 : width < 769 ? 1.8 : width < 800 ? 1.9 : width < 850 ? 1.95 : width < 900 ? 2 : width < 950 ? 2.1 : width < 1000 ? 2.2 : width < 1050 ? 2.3 : width < 1100 ? 2.4 : width < 1150 ? 2.5 : width < 1200 ? 2.6 : width < 1250 ? 2.8 : width < 1300 ? 3 : width < 1400 ? 3.25 : width < 1500 ? 3.5 : width < 1600 ? 3.75 : width < 1800 ? 4 : width < 1900 ? 4.5 : width < 2000 ? 4.75 : width < 2100 ? 5 : width < 2200 ? 5.5 : width < 2400 ? 6 : width < 2600 ? 7 : width < 2800 ? 7.5 : width < 3000 ? 8 : width < 3200 ? 8.5 : width < 3400 ? 9 : width < 3600 ? 9.5 : width < 3800 ? 10 : width < 4000 ? 10.5 : width < 4200 ? 11 : width < 4400 ? 11.5 : width < 4600 ? 12 : width < 4800 ? 12.5 : width < 5000 ? 13 : width < 5200 ? 13.5 : width < 5400 ? 14 : width < 5500 ? 14 : width < 5600 ? 14.5 : 15) * fontSize))
.call((g) => g.selectAll(".tick line").attr("stroke-width", (width < 500 ? .0125 : width < 1000 ? .025 : width < 2000 ? .05 : width < 3000 ? .1 : width < 4000 ? .2 : width < 5000 ? .3 : .4) * fontSize))
.call((g) => g.selectAll(".tick text").attr("color", "black"))
.call((g) => g.selectAll(".tick line").attr("stroke-dasharray", "5 3"));
root
.append("g")
.attr("transform", `translate(${margin.left + chartHeight}, 0)`)
.call(yAxis)
.call((g) => g.select(".domain").remove())
.call((g) => g.selectAll(".tick").attr("color", mapcolors.grid))
.call((g) => g.selectAll(".tick text").attr("font-size", (width < 300 ? .6 : width < 325 ? .7 : width < 350 ? .8 : width < 375 ? .9 : width < 400 ? 1 : width < 450 ? 1.2 : width < 500 ? 1.3 : width < 525 ? 1.4 : width < 550 ? 1.5 : width < 600 ? 1.8 : width < 700 ? 1.8 : width < 769 ? 1.8 : width < 800 ? 1.8 : width < 900 ? 2.5 : width < 1000 ? 3 : width < 1050 ? 3 : width < 1100 ? 3 : width < 1150 ? 3 : width < 1200 ? 3 : width < 1300 ? 3 : width < 1400 ? 3.2 : width < 1500 ? 3.4 : width < 1600 ? 3.6 : width < 1700 ? 3.8 : width < 1800 ? 4 : width < 1900 ? 4.2 : width < 2000 ? 4.5 : width < 2400 ? 5 : width < 2500 ? 5.5 : width < 2600 ? 6 : width < 2700 ? 6 : width < 2800 ? 6.5 : width < 3000 ? 7 : width < 3200 ? 7 : width < 3400 ? 8 : width < 3600 ? 9 : width < 4400 ? 9.5 : width < 4500 ? 10 : width < 4600 ? 10.5 : width < 4700 ? 11 : width < 4800 ? 11.5 : width < 4900 ? 12 : width < 5000 ? 12.5 : width < 5100 ? 13 : width < 5200 ? 13.5 : width < 5400 ? 14 : width < 5600 ? 14 : width < 5800 ? 15 : 14) * fontSize))
.call((g) => g.selectAll(".tick line").attr("stroke-width", (width < 500 ? .0125 : width < 1000 ? .025 : width < 2000 ? .05 : width < 3000 ? .1 : width < 4000 ? .2 : width < 5000 ? .3 : .4) * fontSize))
.call((g) => g.selectAll(".tick text").attr("color", "black"))
.call((g) => g.selectAll(".tick line").attr("stroke-dasharray", "5 3"));
root
.append("text")
.text("Time of day")
.attr("x", -chartHeight / 5 + (width < 275 ? -16 : width < 300 ? -16 : width < 325 ? -16 : width < 350 ? -17 : width < 360 ? -18 : width < 375 ? -20 : width < 400 ? -21 : width < 425 ? -24 : width < 450 ? -25 : width < 475 ? -26 : width < 500 ? -28 : width < 525 ? -31 : width < 550 ? -33 : width < 575 ? -35 : width < 600 ? -38.5 : width < 625 ? -42 : width < 650 ? -45 : width < 675 ? -46 : width < 700 ? -47 : width < 725 ? -48 : width < 740 ? -49 : width < 750 ? -50 : width < 768 ? -51 : width < 769 ? -50 : width < 800 ? -70 : width < 850 ? -78 : width < 900 ? -76 : width < 1000 ? -74 : width < 1025 ? -96 : width < 1050 ? -96 : width < 1075 ? -98 : width < 1100 ? -98 : width < 1125 ? -89 : width < 1150 ? -89 : width < 1175 ? -80 : width < 1200 ? -80 : width < 1225 ? -68 : width < 1250 ? -56 : width < 1275 ? -44 : width < 1300 ? -36 : width < 1350 ? -24 : width < 1400 ? -16 : width < 1450 ? 2 : width < 1500 ? 8 : width < 1600 ? 24 : width < 1650 ? 36 : width < 1700 ? 30 : width < 1750 ? 33 : width < 1800 ? 36 : width < 1900 ? 30 : width < 2000 ? 35 : width < 2200 ? 40 : width < 2300 ? 45 : width < 2400 ? 50 : width < 2500 ? 55 : width < 2600 ? 60 : width < 2800 ? 60 : width < 3000 ? 64 : width < 3100 ? 60 : width < 3200 ? 60 : width < 3300 ? 60 : width < 3400 ? 70 : width < 3500 ? 90 : width < 3600 ? 100 : width < 3700 ? 130 : width < 3800 ? 135 : width < 3900 ? 155 : width < 4000 ? 155 : width < 4200 ? 162 : width < 4300 ? 172 : width < 4400 ? 174 : width < 4500 ? 176 : width < 4600 ? 178 : width < 4700 ? 180 : width < 4800 ? 185 : width < 4900 ? 190 : width < 5000 ? 195 : width < 5200 ? 195 : width < 5400 ? 215 : width < 5600 ? 230 : width < 5800 ? 235 : 250))
.attr("y", margin.left - (width < 275 ? 20 : width < 300 ? 22 : width < 325 ? 26 : width < 350 ? 28 : width < 375 ? 30 : width < 400 ? 32 : width < 425 ? 38 : width < 450 ? 40 : width < 475 ? 42 : width < 500 ? 44 : width < 525 ? 46 : width < 550 ? 48 : width < 575 ? 54 : width < 600 ? 54 : width < 650 ? 54 : width < 675 ? 54 : width < 700 ? 54 : width < 720 ? 54 : width < 740 ? 54 : width < 760 ? 54 : width < 769 ? 54 : width < 800 ? 58 : width < 850 ? 75 : width < 900 ? 75 : width < 1000 ? 90 : width < 1100 ? 92 : width < 1200 ? 94 : width < 1300 ? 96 : width < 1400 ? 98 : width < 1500 ? 105 : width < 1600 ? 110 : width < 1700 ? 120 : width < 1800 ? 125 : width < 1900 ? 130 : width < 2000 ? 145 : width < 2100 ? 150 : width < 2200 ? 155 : width < 2400 ? 160 : width < 2500 ? 170 : width < 2600 ? 180 : width < 2700 ? 190 : width < 2800 ? 200 : width < 3000 ? 210 : width < 3200 ? 220 : width < 3400 ? 250 : width < 3600 ? 260 : width < 3800 ? 270 : width < 4000 ? 280 : width < 4200 ? 290 : width < 4400 ? 300 : width < 4600 ? 320 : width < 4800 ? 340 : width < 4900 ? 360 : width < 5000 ? 380 : width < 5100 ? 390 : width < 5200 ? 390 : width < 5300 ? 395 : width < 5400 ? 395 : width < 5500 ? 395 : width < 5600 ? 395 : 425))
.attr("text-anchor", "middle")
.attr("font-size", fontSize * (width < 275 ? .6 : width < 300 ? .7 : width < 325 ? .8 : width < 350 ? .9 : width < 375 ? 1 : width < 400 ? 1.1 : width < 450 ? 1.2 : width < 500 ? 1.3 : width < 550 ? 1.4 : width < 600 ? 1.5 : width < 650 ? 1.7 : width < 769 ? 1.8 : width < 800 ? 2 : width < 900 ? 2.25 : width < 1000 ? 2.5 : width < 1000 ? 2.75 : width < 1200 ? 3 : width < 1500 ? 3.5 : width < 1600 ? 4 : width < 1700 ? 4.5 : width < 1800 ? 5 : width < 1900 ? 5.5 : width < 2200 ? 6 : width < 2300 ? 6.5 : width < 2400 ? 7 : width < 2500 ? 7.5 : width < 2600 ? 8 : width < 2800 ? 8.5 : width < 3200 ? 9 : width < 3600 ? 9.5 : width < 3900 ? 9.5 : width < 4000 ? 10 : width < 4100 ? 10.5 : width < 4200 ? 11 : width < 4300 ? 11.5 : width < 4400 ? 12 : width < 4500 ? 12.5 : width < 4600 ? 13 : width < 4700 ? 13.5 : width < 4800 ? 14 : width < 4900 ? 14.5 : width < 5000 ? 15 : width < 5200 ? 15 : width < 5400 ? 15 : width < 5600 ? 15 : width < 5800 ? 15 : 16))
.attr("font-family", "sans-serif")
.attr("transform", "rotate(-90)")
.attr("fill", "black");
root
.append("text")
.text("Day of year")
.attr("x", margin.left + majwid / 2 + (width < 275 ? -18 : width < 300 ? -19 : width < 325 ? -21 : width < 350 ? -24 : width < 375 ? -27.5 : width < 400 ? -33 : width < 425 ? -38 : width < 450 ? -39 : width < 475 ? -41 : width < 500 ? -46 : width < 520 ? -50 : width < 540 ? -49 : width < 550 ? -48 : width < 580 ? -50 : width < 600 ? -50 : width < 620 ? -49.5 : width < 640 ? -49 : width < 650 ? -49 : width < 660 ? -49 : width < 680 ? -49 : width < 700 ? -49 : width < 734 ? -49 : width < 769 ? -49 : width < 800 ? -79 : width < 850 ? -97 : width < 900 ? -97 : width < 950 ? -112 : width < 1000 ? -112 : width < 1100 ? -102 : width < 1200 ? -96 : width < 1225 ? -96 : width < 1250 ? -96 : width < 1275 ? -102 : width < 1300 ? -104 : width < 1350 ? -106 : width < 1400 ? -104 : width < 1450 ? -104 : width < 1500 ? -98 : width < 1550 ? -90 : width < 1600 ? -90 : width < 1700 ? -87 : width < 1800 ? -88 : width < 1900 ? -84 : width < 1925 ? -80 : width < 1950 ? -79 : width < 1975 ? -78 : width < 2000 ? -77 : width < 2100 ? -77 : width < 2200 ? -78 : width < 2300 ? -79 : width < 2400 ? -80 : width < 2500 ? -90 : width < 2600 ? -95 : width < 2700 ? -104 : width < 2800 ? -105 : width < 2900 ? -106 : width < 3000 ? -107 : width < 3100 ? -108 : width < 3200 ? -106 : width < 3300 ? -110 : width < 3400 ? -105 : width < 3500 ? -106 : width < 3600 ? -107 : width < 3700 ? -108 : width < 3800 ? -109 : width < 3800 ? -110 : width < 4000 ? -105 : width < 4100 ? -106 : width < 4200 ? -107 : width < 4300 ? -108 : width < 4400 ? -109 : width < 4450 ? -110 : width < 4500 ? -112 : width < 4550 ? -114 : width < 4600 ? -116 : width < 4650 ? -118 : width < 4700 ? -122 : width < 4800 ? -126 : width < 5000 ? -130 : width < 5200 ? -125 : width < 5400 ? -130 : width < 5600 ? -135 : width < 5800 ? -140 : -145))
.attr("y", margin.bottom + (width < 280 ? 92 : width < 290 ? 96 : width < 300 ? 102 : width < 310 ? 106 : width < 320 ? 110 : width < 330 ? 114 : width < 340 ? 120 : width < 350 ? 126 : width < 360 ? 132 : width < 370 ? 136 : width < 380 ? 140 : width < 390 ? 144 : width < 400 ? 150 : width < 410 ? 158 : width < 420 ? 162 : width < 425 ? 164 : width < 430 ? 168 : width < 440 ? 174 : width < 450 ? 180 : width < 460 ? 184 : width < 470 ? 190 : width < 480 ? 194 : width < 490 ? 198 : width < 500 ? 206 : width < 510 ? 214 : width < 520 ? 220 : width < 530 ? 224 : width < 540 ? 228 : width < 550 ? 236 : width < 560 ? 246 : width < 570 ? 252 : width < 580 ? 256 : width < 590 ? 264 : width < 600 ? 272 : width < 610 ? 280 : width < 620 ? 288 : width < 630 ? 294 : width < 640 ? 300 : width < 650 ? 306 : width < 660 ? 312 : width < 670 ? 314 : width < 680 ? 320 : width < 690 ? 326 : width < 700 ? 332 : width < 710 ? 334 : width < 720 ? 340 : width < 730 ? 346 : width < 740 ? 352 : width < 750 ? 356 : width < 760 ? 362 : width < 769 ? 366 : width < 780 ? 385 : width < 790 ? 390 : width < 800 ? 394 : width < 812.5 ? 404 : width < 825 ? 413 : width < 830 ? 424 : width < 840 ? 429 : width < 850 ? 434 : width < 875 ? 438 : width < 900 ? 444 : width < 950 ? 458 : width < 1000 ? 465 : width < 1025 ? 535 : width < 1050 ? 550 : width < 1075 ? 555 : width < 1100 ? 560 : width < 1125 ? 575 : width < 1150 ? 575 : width < 1175 ? 575 : width < 1200 ? 575 : width < 1225 ? 565 : width < 1250 ? 555 : width < 1275 ? 550 : width < 1300 ? 538 : width < 1325 ? 542 : width < 1350 ? 544 : width < 1375 ? 540 : width < 1400 ? 542 : width < 1425 ? 544 : width < 1450 ? 555 : width < 1500 ? 555 : width < 1525 ? 555 : width < 1550 ? 560 : width < 1575 ? 565 : width < 1600 ? 575 : width < 1620 ? 570 : width < 1640 ? 575 : width < 1650 ? 575 : width < 1660 ? 595 : width < 1680 ? 602 : width < 1700 ? 610 : width < 1720 ? 620 : width < 1750 ? 630 : width < 1800 ? 650 : width < 1820 ? 690 : width < 1850 ? 700 : width < 1900 ? 715 : width < 1950 ? 740 : width < 2000 ? 760 : width < 2050 ? 785 : width < 2100 ? 800 : width < 2150 ? 820 : width < 2200 ? 840 : width < 2250 ? 860 : width < 2300 ? 880 : width < 2350 ? 900 : width < 2400 ? 920 : width < 2425 ? 935 : width < 2450 ? 950 : width < 2475 ? 958 : width < 2500 ? 965 : width < 2550 ? 990 : width < 2600 ? 1010 : width < 2650 ? 1040 : width < 2700 ? 1050 : width < 2750 ? 1070 : width < 2800 ? 1090 : width < 2850 ? 1130 : width < 2900 ? 1140 : width < 2950 ? 1150 : width < 3000 ? 1170 : width < 3050 ? 1200 : width < 3100 ? 1220 : width < 3150 ? 1240 : width < 3200 ? 1260 : width < 3250 ? 1280 : width < 3300 ? 1310 : width < 3400 ? 1330 : width < 3450 ? 1320 : width < 3500 ? 1340 : width < 3550 ? 1360 : width < 3600 ? 1380 : width < 3650 ? 1400 : width < 3700 ? 1410 : width < 3800 ? 1415 : width < 3900 ? 1420 : width < 4000 ? 1460 : width < 4050 ? 1480 : width < 4100 ? 1500 : width < 4150 ? 1515 : width < 4200 ? 1530 : width < 4250 ? 1555 : width < 4300 ? 1575 : width < 4400 ? 1600 : width < 4500 ? 1640 : width < 4600 ? 1680 : width < 4700 ? 1720 : width < 4750 ? 1720 : width < 4800 ? 1740 : width < 4850 ? 1760 : width < 4900 ? 1780 : width < 4950 ? 1800 : width < 5000 ? 1820 : width < 5050 ? 1840 : width < 5100 ? 1880 : width < 5150 ? 1890 : width < 5200 ? 1900 : width < 5250 ? 1920 : width < 5300 ? 1940 : width < 5350 ? 1960 : width < 5400 ? 1980 : width < 5450 ? 2000 : width < 5500 ? 2010 : width < 5520 ? 2020 : width < 5540 ? 2020 : width < 5560 ? 2020 : width < 5580 ? 2040 : width < 5600 ? 2060 : width < 5620 ? 2090 : width < 5640 ? 2100 : width < 5660 ? 2110 : width < 5680 ? 2110 : width < 5700 ? 2120 : width < 5750 ? 2130 : width < 5800 ? 2150 : 2150))
.attr("text-anchor", "middle")
.attr("font-size", fontSize * (width < 250 ? .6 : width < 275 ? .7 : width < 300 ? .8 : width < 325 ? .9 : width < 350 ? 1 : width < 375 ? 1.1 : width < 400 ? 1.2 : width < 425 ? 1.3 : width < 490 ? 1.4 : width < 500 ? 1.5 : width < 550 ? 1.6 : width < 769 ? 1.7 : width < 800 ? 1.8 : width < 900 ? 2 : width < 1000 ? 2.25 : width < 1100 ? 2.5 : width < 1200 ? 2.75 : width < 1300 ? 3 : width < 1400 ? 3 : width < 1500 ? 3.5 : width < 1600 ? 4 : width < 1700 ? 4 : width < 1800 ? 4.5 : width < 1900 ? 5 : width < 2000 ? 5.5 : width < 2100 ? 5.5 : width < 2200 ? 6 : width < 2300 ? 6 : width < 2400 ? 6.5 : width < 2500 ? 6.5 : width < 2600 ? 7 : width < 2700 ? 7.5 : width < 2800 ? 7.5 : width < 2900 ? 8 : width < 3000 ? 8.5 : width < 3000 ? 8.5 : width < 3200 ? 8.5 : width < 3300 ? 9 : width < 3400 ? 9.5 : width < 3500 ? 9.5 : width < 3600 ? 9.5 : width < 3700 ? 9.5 : width < 3800 ? 10 : width < 3900 ? 10.5 : width < 4000 ? 11 : width < 4200 ? 11.5 : width < 4400 ? 12 : width < 4600 ? 12.5 : width < 4800 ? 13 : width < 5000 ? 13.5 : width < 5200 ? 14 : width < 5400 ? 14.5 : width < 5600 ? 15 : width < 5800 ? 15.5 : 16))
.attr("font-family", "sans-serif")
.attr("fill", "black");
const data = yearDates(year)
.map((d) => [d, dayLength(d, latitude)])
.filter(([_, d]) => d > 0);
/* Render separate polygons for each continuous sequence of
* days with more than 0 hours of day light
*/
const polys = [];
let currentPoly = [];
for (let i = 0; i < data.length; i++) {
const currentDate = data[i][0];
const prevDate = (data[i - 1] || [])[0];
if (
i === 0 ||
currentDate.getTime() - prevDate.getTime() < 3600 * 24 * 1000 * 1.5
) {
currentPoly.push(data[i]);
} else {
polys.push(currentPoly);
currentPoly = [data[i]];
}
}
polys.push(currentPoly);
polys.forEach((p) => {
const points = [
...p.map(([d, l]) => `${xScale(d)},${yScale(12 - l / 2)}`),
...p.reverse().map(([d, l]) => `${xScale(d)},${yScale(12 + l / 2)}`)
].join(" ");
root.append("polygon").attr("points", points).attr("fill", mapcolors.day);
});
/* Legend */
const legend = root
.append("g")
.attr("transform", `translate(${margin.left + chartWidth / 2 - 64})`);
legend
.append("rect")
.attr("x", (width < 300 ? -19 : width < 310 ? -21 : width < 320 ? -23 : width < 330 ? -25 : width < 340 ? -27 : width < 350 ? -29 : width < 360 ? -31 : width < 370 ? -33 : width < 380 ? -35 : width < 390 ? -37 : width < 400 ? -41 : width < 410 ? -49 : width < 420 ? -55 : width < 430 ? -57 : width < 440 ? -60 : width < 450 ? -62 : width < 460 ? -64 : width < 470 ? -66 : width < 480 ? -68 : width < 490 ? -70 : width < 500 ? -72 : width < 510 ? -77 : width < 520 ? -81 : width < 530 ? -85 : width < 540 ? -92 : width < 550 ? -98 : width < 560 ? -105 : width < 570 ? -109 : width < 580 ? -113 : width < 590 ? -117 : width < 600 ? -121 : width < 610 ? -125 : width < 620 ? -129 : width < 630 ? -133 : width < 640 ? -137 : width < 650 ? -141 : width < 660 ? -144 : width < 670 ? -148 : width < 680 ? -151 : width < 690 ? -155 : width < 700 ? -157 : width < 710 ? -162 : width < 720 ? -164 : width < 730 ? -167 : width < 740 ? -169 : width < 750 ? -171 : width < 760 ? -174 : width < 769 ? -176 : width < 800 ? -210 : width < 825 ? -225 : width < 850 ? -245 : width < 875 ? -244 : width < 900 ? -250 : width < 1000 ? -260 : width < 1100 ? -306 : width < 1200 ? -312 : width < 1225 ? -318 : width < 1250 ? -314 : width < 1275 ? -310 : width < 1300 ? -306 : width < 1350 ? -312 : width < 1400 ? -310 : width < 1450 ? -315 : width < 1500 ? -326 : width < 1550 ? -338 : width < 1600 ? -350 : width < 1650 ? -365 : width < 1700 ? -375 : width < 1800 ? -400 : width < 1850 ? -450 : width < 1900 ? -470 : width < 2000 ? -480 : width < 2100 ? -520 : width < 2200 ? -550 : width < 2300 ? -590 : width < 2400 ? -620 : width < 2500 ? -650 : width < 2600 ? -680 : width < 2700 ? -710 : width < 2800 ? -740 : width < 2900 ? -770 : width < 3000 ? -800 : width < 3100 ? -850 : width < 3200 ? -860 : width < 3300 ? -900 : width < 3400 ? -910 : width < 3500 ? -920 : width < 3600 ? -930 : width < 3800 ? -950 : width < 4000 ? -980 : width < 4200 ? -1020 : width < 4400 ? -1060 : width < 4600 ? -1120 : width < 4800 ? -1160 : width < 5000 ? -1200 : width < 5200 ? -1276 : width < 5500 ? -1320 : width < 5600 ? -1350 : width < 5800 ? -1400 : -1500))
.attr("y", (width < 300 ? 109 : width < 310 ? 113 : width < 320 ? 117 : width < 330 ? 121 : width < 340 ? 125 : width < 350 ? 129 : width < 360 ? 133 : width < 370 ? 137 : width < 380 ? 141 : width < 390 ? 145 : width < 400 ? 151 : width < 410 ? 160 : width < 420 ? 166 : width < 430 ? 170 : width < 440 ? 174 : width < 450 ? 178 : width < 460 ? 182 : width < 470 ? 186 : width < 480 ? 188 : width < 490 ? 191 : width < 500 ? 194 : width < 510 ? 198 : width < 520 ? 204 : width < 530 ? 210 : width < 540 ? 216.5 : width < 550 ? 221 : width < 560 ? 227 : width < 570 ? 233 : width < 580 ? 239 : width < 590 ? 247 : width < 600 ? 254 : width < 610 ? 259 : width < 620 ? 267 : width < 630 ? 271 : width < 640 ? 275 : width < 650 ? 279 : width < 660 ? 284 : width < 670 ? 288 : width < 680 ? 292 : width < 690 ? 296 : width < 700 ? 300 : width < 710 ? 305 : width < 720 ? 309 : width < 730 ? 311 : width < 740 ? 316 : width < 750 ? 320 : width < 760 ? 323 : width < 769 ? 327 : width < 800 ? 360 : width < 825 ? 379 : width < 850 ? 399 : width < 875 ? 404 : width < 900 ? 410 : width < 950 ? 416 : width < 1000 ? 420 : width < 1050 ? 500 : width < 1100 ? 504 : width < 1200 ? 512 : width < 1225 ? 497 : width < 1250 ? 488 : width < 1275 ? 478 : width < 1300 ? 472 : width < 1350 ? 479 : width < 1400 ? 478 : width < 1450 ? 477 : width < 1500 ? 482 : width < 1550 ? 491 : width < 1600 ? 500 : width < 1650 ? 500 : width < 1700 ? 522 : width < 1800 ? 564 : width < 1850 ? 590 : width < 1900 ? 624 : width < 2000 ? 640 : width < 2100 ? 680 : width < 2200 ? 718 : width < 2300 ? 744 : width < 2400 ? 784 : width < 2500 ? 800 : width < 2600 ? 840 : width < 2700 ? 884 : width < 2800 ? 914 : width < 2900 ? 952 : width < 3000 ? 984 : width < 3100 ? 1022 : width < 3200 ? 1052 : width < 3300 ? 1102 : width < 3400 ? 1142 : width < 3500 ? 1152 : width < 3600 ? 1160 : width < 3800 ? 1180 : width < 4000 ? 1200 : width < 4200 ? 1255 : width < 4400 ? 1306 : width < 4600 ? 1370 : width < 4800 ? 1430 : width < 5000 ? 1500 : width < 5200 ? 1580 : width < 5500 ? 1658 : width < 5600 ? 1690 : width < 5800 ? 1760 : 1800))
.attr("rx", 5)
.attr("width", fontSize * (width < 300 ? .3 : width < 400 ? .32 : width < 490 ? .36 : width < 500 ? .4 : width < 540 ? .4 : width < 550 ? .61 : width < 600 ? .62 : width < 660 ? .63 : width < 680 ? .64 : width < 720 ? .7 : width < 750 ? .75 : width < 1200 ? .8 : width < 1600 ? .98 : width < 1800 ? 1.2 : width < 2000 ? 1.4 : width < 2200 ? 1.6 : width < 2400 ? 1.8 : width < 5600 ? 1.9 : width < 5800 ? 1.9 : 2) + majwid / 100)
.attr("height", fontSize * (width < 300 ? .3 : width < 400 ? .32 : width < 490 ? .36 : width < 500 ? .4 : width < 540 ? .4 : width < 550 ? .61 : width < 600 ? .62 : width < 660 ? .63 : width < 680 ? .64 : width < 720 ? .7 : width < 750 ? .75 : width < 1200 ? .8 : width < 1600 ? .98 : width < 1800 ? 1.2 : width < 2000 ? 1.4 : width < 2200 ? 1.6 : width < 2400 ? 1.8 : width < 5600 ? 1.9 : width < 5800 ? 1.9 : 2) + majwid / 100)
.attr("fill", mapcolors.night);
legend
.append("text")
.attr("x", (width < 300 ? -10 : width < 310 ? -12 : width < 320 ? -14 : width < 330 ? -16 : width < 340 ? -18 : width < 350 ? -20 : width < 360 ? -22 : width < 370 ? -24 : width < 380 ? -26 : width < 390 ? -28 : width < 400 ? -32 : width < 410 ? -39 : width < 420 ? -45 : width < 430 ? -47 : width < 440 ? -49 : width < 450 ? -51 : width < 460 ? -53 : width < 470 ? -55 : width < 480 ? -57 : width < 490 ? -59 : width < 500 ? -60 : width < 510 ? -64 : width < 520 ? -68 : width < 530 ? -72 : width < 540 ? -79 : width < 550 ? -83 : width < 560 ? -88 : width < 570 ? -92 : width < 580 ? -96 : width < 590 ? -100 : width < 600 ? -104 : width < 610 ? -108 : width < 620 ? -112 : width < 630 ? -116 : width < 640 ? -120 : width < 650 ? -124 : width < 660 ? -127 : width < 670 ? -130 : width < 680 ? -133 : width < 690 ? -137 : width < 700 ? -139 : width < 710 ? -144 : width < 720 ? -146 : width < 730 ? -148 : width < 740 ? -150 : width < 750 ? -152 : width < 760 ? -154 : width < 769 ? -156 : width < 800 ? -188 : width < 825 ? -203 : width < 850 ? -223 : width < 875 ? -222 : width < 900 ? -228 : width < 1000 ? -236 : width < 1100 ? -282 : width < 1200 ? -288 : width < 1225 ? -290 : width < 1250 ? -286 : width < 1275 ? -282 : width < 1300 ? -278 : width < 1350 ? -282 : width < 1400 ? -281 : width < 1450 ? -286 : width < 1500 ? -296 : width < 1550 ? -307 : width < 1600 ? -318 : width < 1650 ? -328 : width < 1700 ? -338 : width < 1800 ? -360 : width < 1850 ? -410 : width < 1900 ? -425 : width < 2000 ? -435 : width < 2100 ? -470 : width < 2200 ? -500 : width < 2300 ? -535 : width < 2400 ? -560 : width < 2500 ? -590 : width < 2600 ? -620 : width < 2700 ? -650 : width < 2800 ? -680 : width < 2900 ? -710 : width < 3000 ? -740 : width < 3100 ? -785 : width < 3200 ? -795 : width < 3300 ? -830 : width < 3400 ? -840 : width < 3500 ? -850 : width < 3600 ? -860 : width < 3800 ? -880 : width < 4000 ? -900 : width < 4200 ? -940 : width < 4400 ? -980 : width < 4600 ? -1040 : width < 4800 ? -1075 : width < 5000 ? -1115 : width < 5200 ? -1184 : width < 5500 ? -1224 : width < 5600 ? -1252 : width < 5800 ? -1300 : -1400))
.attr("y", (width < 300 ? 116 : width < 310 ? 120.4 : width < 320 ? 124.5 : width < 330 ? 128.6 : width < 340 ? 132.8 : width < 350 ? 136.6 : width < 360 ? 140.5 : width < 370 ? 145 : width < 380 ? 149 : width < 390 ? 153 : width < 400 ? 159 : width < 410 ? 168.5 : width < 420 ? 174.5 : width < 430 ? 178.5 : width < 440 ? 182.5 : width < 450 ? 187 : width < 460 ? 191 : width < 470 ? 195 : width < 480 ? 197 : width < 490 ? 200.5 : width < 500 ? 204 : width < 510 ? 208 : width < 520 ? 214 : width < 530 ? 220 : width < 540 ? 227 : width < 550 ? 233 : width < 560 ? 240 : width < 570 ? 246 : width < 580 ? 252 : width < 590 ? 260 : width < 600 ? 267 : width < 610 ? 272.5 : width < 620 ? 281 : width < 630 ? 285 : width < 640 ? 289 : width < 650 ? 293.5 : width < 660 ? 298.5 : width < 670 ? 303 : width < 680 ? 307 : width < 690 ? 312 : width < 700 ? 316 : width < 710 ? 321 : width < 720 ? 325 : width < 730 ? 328 : width < 740 ? 333 : width < 750 ? 337 : width < 760 ? 341 : width < 769 ? 345 : width < 800 ? 379 : width < 825 ? 399 : width < 850 ? 419 : width < 875 ? 425 : width < 900 ? 431 : width < 950 ? 437 : width < 1000 ? 442 : width < 1050 ? 522 : width < 1100 ? 526 : width < 1125 ? 534.5 : width < 1200 ? 535 : width < 1225 ? 523 : width < 1250 ? 514 : width < 1275 ? 504 : width < 1300 ? 498 : width < 1350 ? 505 : width < 1400 ? 505 : width < 1450 ? 506 : width < 1500 ? 511 : width < 1550 ? 521 : width < 1600 ? 530 : width < 1650 ? 531 : width < 1700 ? 554 : width < 1800 ? 598 : width < 1825 ? 628 : width < 1850 ? 629 : width < 1900 ? 663 : width < 2000 ? 680 : width < 2100 ? 725 : width < 2150 ? 763 : width < 2200 ? 764 : width < 2300 ? 794 : width < 2400 ? 835 : width < 2500 ? 855 : width < 2600 ? 896 : width < 2700 ? 943 : width < 2800 ? 975 : width < 2900 ? 1014 : width < 3000 ? 1047 : width < 3100 ? 1088 : width < 3200 ? 1120 : width < 3300 ? 1170 : width < 3400 ? 1210 : width < 3500 ? 1220 : width < 3600 ? 1230 : width < 3800 ? 1250 : width < 4000 ? 1273 : width < 4200 ? 1330 : width < 4400 ? 1386 : width < 4600 ? 1453 : width < 4800 ? 1515 : width < 5000 ? 1585 : width < 5200 ? 1668 : width < 5300 ? 1746 : width < 5500 ? 1750 : width < 5600 ? 1783 : width < 5800 ? 1854 : 1908))
.attr("font-size", fontSize * (width < 480 ? .46 : width < 490 ? .48 : width < 500 ? .50 : width < 510 ? .52 : width < 520 ? .54 : width < 530 ? .56 : width < 540 ? .58 : width < 550 ? .60 : width < 560 ? .62 : width < 570 ? .64 : width < 580 ? .66 : width < 590 ? .68 : width < 600 ? .70 : width < 610 ? .72 : width < 620 ? .74 : width < 630 ? .76 : width < 640 ? .78 : width < 650 ? .80 : width < 660 ? .82 : width < 670 ? .89 : width < 680 ? .9 : width < 690 ? .91 : width < 700 ? .92 : width < 710 ? .94 : width < 720 ? .96 : width < 730 ? .98 : width < 740 ? 1 : width < 750 ? 1.02 : width < 760 ? 1.04 : width < 769 ? 1.05 : width < 800 ? 1.2 : width < 1200 ? 1.4 : width < 1400 ? 1.5 : width < 1600 ? 1.75 : width < 1800 ? 2 : width < 2000 ? 2.5 : width < 2200 ? 3 : width < 2400 ? 3.5 : width < 2600 ? 4 : width < 3000 ? 4.5 : width < 4200 ? 5 : width < 4400 ? 5.5 : width < 5800 ? 6 : 8) + majwid / 100)
.attr("font-family", "sans-serif")
.text("Night");
legend
.append("rect")
.attr("x", (width < 300 ? -19 : width < 310 ? -21 : width < 320 ? -23 : width < 330 ? -25 : width < 340 ? -27 : width < 350 ? -29 : width < 360 ? -31 : width < 370 ? -33 : width < 380 ? -35 : width < 390 ? -37 : width < 400 ? -41 : width < 410 ? -49 : width < 420 ? -55 : width < 430 ? -57 : width < 440 ? -60 : width < 450 ? -62 : width < 460 ? -64 : width < 470 ? -66 : width < 480 ? -68 : width < 490 ? -70 : width < 500 ? -72 : width < 510 ? -77 : width < 520 ? -81 : width < 530 ? -85 : width < 540 ? -92 : width < 550 ? -98 : width < 560 ? -105 : width < 570 ? -109 : width < 580 ? -113 : width < 590 ? -117 : width < 600 ? -121 : width < 610 ? -125 : width < 620 ? -129 : width < 630 ? -133 : width < 640 ? -137 : width < 650 ? -141 : width < 660 ? -144 : width < 670 ? -148 : width < 680 ? -151 : width < 690 ? -155 : width < 700 ? -157 : width < 710 ? -162 : width < 720 ? -164 : width < 730 ? -167 : width < 740 ? -169 : width < 750 ? -171 : width < 760 ? -174 : width < 769 ? -176 : width < 800 ? -210 : width < 825 ? -225 : width < 850 ? -245 : width < 875 ? -244 : width < 900 ? -250 : width < 1000 ? -260 : width < 1100 ? -306 : width < 1200 ? -312 : width < 1225 ? -318 : width < 1250 ? -314 : width < 1275 ? -310 : width < 1300 ? -306 : width < 1350 ? -312 : width < 1400 ? -310 : width < 1450 ? -315 : width < 1500 ? -326 : width < 1550 ? -338 : width < 1600 ? -350 : width < 1650 ? -365 : width < 1700 ? -375 : width < 1800 ? -400 : width < 1850 ? -450 : width < 1900 ? -470 : width < 2000 ? -480 : width < 2100 ? -520 : width < 2200 ? -550 : width < 2300 ? -590 : width < 2400 ? -620 : width < 2500 ? -650 : width < 2600 ? -680 : width < 2700 ? -710 : width < 2800 ? -740 : width < 2900 ? -770 : width < 3000 ? -800 : width < 3100 ? -850 : width < 3200 ? -860 : width < 3300 ? -900 : width < 3400 ? -910 : width < 3500 ? -920 : width < 3600 ? -930 : width < 3800 ? -950 : width < 4000 ? -980 : width < 4200 ? -1020 : width < 4400 ? -1060 : width < 4600 ? -1120 : width < 4800 ? -1160 : width < 5000 ? -1200 : width < 5200 ? -1276 : width < 5500 ? -1320 : width < 5600 ? -1350 : width < 5800 ? -1400 : -1500))
.attr("y", (width < 300 ? 120 : width < 310 ? 124 : width < 320 ? 128 : width < 330 ? 132 : width < 340 ? 136 : width < 350 ? 140 : width < 360 ? 144 : width < 370 ? 148 : width < 380 ? 152 : width < 390 ? 156 : width < 400 ? 162 : width < 410 ? 173 : width < 420 ? 179 : width < 430 ? 183 : width < 440 ? 187 : width < 450 ? 191 : width < 460 ? 195 : width < 470 ? 199 : width < 480 ? 201 : width < 490 ? 204 : width < 500 ? 207 : width < 510 ? 213 : width < 520 ? 219 : width < 530 ? 225 : width < 540 ? 231.5 : width < 550 ? 239 : width < 560 ? 245 : width < 570 ? 251 : width < 580 ? 257 : width < 590 ? 265 : width < 600 ? 272 : width < 610 ? 281 : width < 620 ? 289 : width < 630 ? 293 : width < 640 ? 297 : width < 650 ? 301 : width < 660 ? 306 : width < 670 ? 310 : width < 680 ? 314 : width < 690 ? 318 : width < 700 ? 322 : width < 710 ? 327 : width < 720 ? 331 : width < 730 ? 334 : width < 740 ? 339 : width < 750 ? 343 : width < 760 ? 346 : width < 769 ? 350 : width < 800 ? 386 : width < 825 ? 405 : width < 850 ? 425 : width < 875 ? 432 : width < 900 ? 438 : width < 950 ? 446 : width < 1000 ? 450 : width < 1050 ? 530 : width < 1100 ? 538 : width < 1200 ? 545 : width < 1225 ? 532 : width < 1250 ? 527 : width < 1275 ? 513 : width < 1300 ? 512 : width < 1350 ? 519 : width < 1400 ? 512 : width < 1450 ? 517 : width < 1500 ? 526 : width < 1550 ? 536 : width < 1600 ? 545 : width < 1650 ? 545 : width < 1700 ? 568 : width < 1800 ? 610 : width < 1850 ? 645 : width < 1900 ? 680 : width < 2000 ? 700 : width < 2100 ? 746 : width < 2200 ? 782 : width < 2300 ? 812 : width < 2400 ? 852 : width < 2500 ? 885 : width < 2600 ? 925 : width < 2700 ? 980 : width < 2800 ? 1010 : width < 2900 ? 1052 : width < 3000 ? 1080 : width < 3100 ? 1114 : width < 3200 ? 1164 : width < 3300 ? 1212 : width < 3400 ? 1252 : width < 3500 ? 1262 : width < 3600 ? 1280 : width < 3800 ? 1299 : width < 4000 ? 1330 : width < 4200 ? 1385 : width < 4400 ? 1440 : width < 4600 ? 1510 : width < 4800 ? 1580 : width < 5000 ? 1650 : width < 5200 ? 1725 : width < 5500 ? 1808 : width < 5600 ? 1840 : width < 5800 ? 1910 : 1982))
.attr("rx", 5)
.attr("width", fontSize * (width < 300 ? .3 : width < 400 ? .32 : width < 490 ? .36 : width < 500 ? .4 : width < 540 ? .4 : width < 550 ? .61 : width < 600 ? .62 : width < 660 ? .63 : width < 680 ? .64 : width < 720 ? .7 : width < 750 ? .75 : width < 1200 ? .8 : width < 1600 ? .98 : width < 1800 ? 1.2 : width < 2000 ? 1.4 : width < 2200 ? 1.6 : width < 2400 ? 1.8 : width < 5600 ? 1.9 : width < 5800 ? 1.9 : 2) + majwid / 100)
.attr("height", fontSize * (width < 300 ? .3 : width < 400 ? .32 : width < 490 ? .36 : width < 500 ? .4 : width < 540 ? .4 : width < 550 ? .61 : width < 600 ? .62 : width < 660 ? .63 : width < 680 ? .64 : width < 720 ? .7 : width < 750 ? .75 : width < 1200 ? .8 : width < 1600 ? .98 : width < 1800 ? 1.2 : width < 2000 ? 1.4 : width < 2200 ? 1.6 : width < 2400 ? 1.8 : width < 5600 ? 1.9 : width < 5800 ? 1.9 : 2) + majwid / 100)
.attr("fill", mapcolors.day);
legend
.append("text")
.attr("x", (width < 300 ? -10 : width < 310 ? -12 : width < 320 ? -14 : width < 330 ? -16 : width < 340 ? -18 : width < 350 ? -20 : width < 360 ? -22 : width < 370 ? -24 : width < 380 ? -26 : width < 390 ? -28 : width < 400 ? -32 : width < 410 ? -39 : width < 420 ? -45 : width < 430 ? -47 : width < 440 ? -49 : width < 450 ? -51 : width < 460 ? -53 : width < 470 ? -55 : width < 480 ? -57 : width < 490 ? -59 : width < 500 ? -60 : width < 510 ? -64 : width < 520 ? -68 : width < 530 ? -72 : width < 540 ? -79 : width < 550 ? -83 : width < 560 ? -88 : width < 570 ? -92 : width < 580 ? -96 : width < 590 ? -100 : width < 600 ? -104 : width < 610 ? -108 : width < 620 ? -112 : width < 630 ? -116 : width < 640 ? -120 : width < 650 ? -124 : width < 660 ? -127 : width < 670 ? -130 : width < 680 ? -133 : width < 690 ? -137 : width < 700 ? -139 : width < 710 ? -144 : width < 720 ? -146 : width < 730 ? -148 : width < 740 ? -150 : width < 750 ? -152 : width < 760 ? -154 : width < 769 ? -156 : width < 800 ? -188 : width < 825 ? -203 : width < 850 ? -223 : width < 875 ? -222 : width < 900 ? -228 : width < 1000 ? -236 : width < 1100 ? -282 : width < 1200 ? -288 : width < 1225 ? -290 : width < 1250 ? -286 : width < 1275 ? -282 : width < 1300 ? -278 : width < 1350 ? -282 : width < 1400 ? -281 : width < 1450 ? -286 : width < 1500 ? -296 : width < 1550 ? -307 : width < 1600 ? -318 : width < 1650 ? -328 : width < 1700 ? -338 : width < 1800 ? -360 : width < 1850 ? -410 : width < 1900 ? -425 : width < 2000 ? -435 : width < 2100 ? -470 : width < 2200 ? -500 : width < 2300 ? -535 : width < 2400 ? -560 : width < 2500 ? -590 : width < 2600 ? -620 : width < 2700 ? -650 : width < 2800 ? -680 : width < 2900 ? -710 : width < 3000 ? -740 : width < 3100 ? -785 : width < 3200 ? -795 : width < 3300 ? -830 : width < 3400 ? -840 : width < 3500 ? -850 : width < 3600 ? -860 : width < 3800 ? -880 : width < 4000 ? -900 : width < 4200 ? -940 : width < 4400 ? -980 : width < 4600 ? -1040 : width < 4800 ? -1075 : width < 5000 ? -1115 : width < 5200 ? -1184 : width < 5500 ? -1224 : width < 5600 ? -1252 : width < 5800 ? -1300 : -1400))
.attr("y", (width < 300 ? 127 : width < 310 ? 131.4 : width < 320 ? 135.5 : width < 330 ? 139.6 : width < 340 ? 143.8 : width < 350 ? 147.6 : width < 360 ? 151.5 : width < 370 ? 156 : width < 380 ? 160 : width < 390 ? 164 : width < 400 ? 170 : width < 410 ? 181.5 : width < 420 ? 187.5 : width < 430 ? 191.5 : width < 440 ? 195.5 : width < 450 ? 200 : width < 460 ? 204 : width < 470 ? 208 : width < 480 ? 210 : width < 490 ? 213.5 : width < 500 ? 217 : width < 510 ? 223 : width < 520 ? 229 : width < 530 ? 235 : width < 540 ? 242 : width < 550 ? 251 : width < 560 ? 258 : width < 570 ? 264 : width < 580 ? 270 : width < 590 ? 278 : width < 600 ? 285 : width < 610 ? 294.5 : width < 620 ? 303 : width < 630 ? 307 : width < 640 ? 311 : width < 650 ? 315.5 : width < 660 ? 320.5 : width < 670 ? 325 : width < 680 ? 329 : width < 690 ? 334 : width < 700 ? 338 : width < 710 ? 343 : width < 720 ? 347 : width < 730 ? 351 : width < 740 ? 356 : width < 750 ? 360 : width < 760 ? 364 : width < 769 ? 368 : width < 800 ? 405 : width < 825 ? 425 : width < 850 ? 445 : width < 875 ? 453 : width < 900 ? 459 : width < 950 ? 467 : width < 1000 ? 471 : width < 1050 ? 552 : width < 1100 ? 560 : width < 1125 ? 567.5 : width < 1200 ? 568 : width < 1225 ? 557 : width < 1250 ? 552 : width < 1275 ? 539 : width < 1300 ? 538 : width < 1350 ? 545 : width < 1400 ? 539 : width < 1450 ? 545 : width < 1500 ? 555 : width < 1550 ? 565 : width < 1600 ? 574 : width < 1650 ? 578 : width < 1700 ? 602 : width < 1750 ? 644 : width < 1800 ? 644 : width < 1825 ? 683 : width < 1850 ? 684 : width < 1900 ? 719 : width < 2000 ? 739 : width < 2100 ? 791 : width < 2150 ? 827 : width < 2200 ? 828 : width < 2300 ? 862 : width < 2400 ? 903 : width < 2500 ? 940 : width < 2600 ? 980 : width < 2700 ? 1039 : width < 2800 ? 1070 : width < 2900 ? 1112 : width < 3000 ? 1141 : width < 3100 ? 1180 : width < 3200 ? 1230 : width < 3300 ? 1279 : width < 3400 ? 1320 : width < 3500 ? 1330 : width < 3600 ? 1350 : width < 3800 ? 1372 : width < 4000 ? 1402 : width < 4200 ? 1460 : width < 4400 ? 1519 : width < 4600 ? 1591 : width < 4800 ? 1664 : width < 5000 ? 1735 : width < 5200 ? 1812 : width < 5300 ? 1894 : width < 5500 ? 1900 : width < 5600 ? 1932 : width < 5800 ? 2004 : 2090))
.attr("font-size", fontSize * (width < 480 ? .46 : width < 490 ? .48 : width < 500 ? .50 : width < 510 ? .52 : width < 520 ? .54 : width < 530 ? .56 : width < 540 ? .58 : width < 550 ? .60 : width < 560 ? .62 : width < 570 ? .64 : width < 580 ? .66 : width < 590 ? .68 : width < 600 ? .70 : width < 610 ? .72 : width < 620 ? .74 : width < 630 ? .76 : width < 640 ? .78 : width < 650 ? .80 : width < 660 ? .82 : width < 670 ? .89 : width < 680 ? .9 : width < 690 ? .91 : width < 700 ? .92 : width < 710 ? .94 : width < 720 ? .96 : width < 730 ? .98 : width < 740 ? 1 : width < 750 ? 1.02 : width < 760 ? 1.04 : width < 769 ? 1.05 : width < 800 ? 1.2 : width < 1200 ? 1.4 : width < 1400 ? 1.5 : width < 1600 ? 1.75 : width < 1800 ? 2 : width < 2000 ? 2.5 : width < 2200 ? 3 : width < 2400 ? 3.5 : width < 2600 ? 4 : width < 3000 ? 4.5 : width < 4200 ? 5 : width < 4400 ? 5.5 : width < 5800 ? 6 : 8) + majwid / 100)
.attr("font-family", "sans-serif")
.text("Day");
/* Time and date controls */
const dateLine = root.append("g");
const updateControlPositions = () => {
dateLine
.select("line")
.attr("y1", yScale(0))
.attr("x1", xScale(date))
.attr("y2", yScale(24))
.attr("x2", xScale(date));
dateLine
.select("rect")
.attr("y", yScale(0))
.attr("width", width < 500 ? 10 : width < 1000 ? 20 : width < 2000 ? 40 : width < 3000 ? 60 : width < 4000 ? 80 : width < 5500 ? 110 : 120)
.attr("x", xScale(date) - (width < 500 ? 2.5 : width < 1000 ? 5 : width < 2000 ? 10 : width < 3000 ? 15 : width < 4000 ? 20 : width < 5000 ? 25 : 30));
root
.select("#time-control")
.attr("cy", yScale(hour))
.attr("cx", xScale(date));
};
const dispatchDateHourChange = () => {
const detail = { date, hour };
const changeEvent = new CustomEvent(EventType.DateHourChange, {
detail,
bubbles: true
});
root.node().dispatchEvent(changeEvent);
};
const handleDateLineDrag = ({ x }) => {
date = xScale.invert(x);
updateControlPositions();
dispatchDateHourChange();
};
const handleTimeCircleDrag = ({ y }) => {
hour = yScale.invert(y);
updateControlPositions();
dispatchDateHourChange();
};
dateLine.append("line").attr("stroke-width", (width < 400 ? 3 : width < 500 ? 4 : width < 600 ? 5 : width < 700 ? 6 : width < 800 ? 7 : width < 900 ? 8 : width < 1000 ? 9 : width < 1100 ? 10 : width < 1200 ? 11 : width < 1300 ? 12 : width < 1400 ? 14 : width < 1500 ? 16 : width < 1600 ? 18 : width < 1700 ? 20 : width < 1800 ? 22 : width < 1900 ? 24 : width < 2000 ? 28 : width < 2300 ? 32 : width < 2600 ? 36 : width < 3200 ? 40 : width < 3600 ? 44 : width < 3800 ? 48 : width < 4400 ? 50 : width < 4400 ? 54 : width < 5000 ? 58 : width < 5600 ? 60 : majwid / 88)).attr("stroke", "red");
dateLine
.append("rect")
.attr("height", chartWidth)
.attr("width", 8)
.attr("fill", "rgba(0, 0, 0, 0)")
.style("cursor", "row-resize")
.call(d3.drag().on("drag", handleDateLineDrag));
root
.append("circle")
.attr("id", "time-control")
.attr("r", (width < 300 ? 5 : width < 400 ? 6 : width < 500 ? 7 : width < 600 ? 8 : width < 700 ? 10 : width < 800 ? 12 : width < 900 ? 14 : width < 1000 ? 16 : width < 1100 ? 18 : width < 1200 ? 20 : width < 1300 ? 22 : width < 1400 ? 24 : width < 1500 ? 26 : width < 1600 ? 28 : width < 1700 ? 30 : width < 1800 ? 32 : width < 1800 ? 34 : width < 1900 ? 36 : width < 2000 ? 38 : width < 2300 ? 42 : width < 2600 ? 46 : width < 3200 ? 50 : width < 3600 ? 54 : width < 3800 ? 60 : width < 4000 ? 66 : width < 4200 ? 72 : width < 4400 ? 78 : width < 5000 ? 84 : width < 5600 ? 90 : majwid / 59))
.attr("fill", "red")
.attr("stroke-width", width < 300 ? .3 : width < 400 ? .4 : width < 500 ? .5 : width < 600 ? .6 : width < 700 ? .7 : width < 800 ? .8 : width < 900 ? .9 : width < 1000 ? 1 : width < 1100 ? 1.1 : width < 1200 ? 1.2 : width < 1300 ? 1.3 : width < 1400 ? 1.4 : width < 1500 ? 1.5 : width < 1600 ? 1.6 : width < 1700 ? 1.7 : width < 1800 ? 1.8 : width < 1900 ? 1.9 : width < 2000 ? 2.0 : width < 2100 ? 2.1 : width < 2200 ? 2.2 : width < 2300 ? 2.3 : width < 2400 ? 2.4 : width < 2500 ? 2.5 : width < 2600 ? 2.6 : width < 2700 ? 2.7 : width < 2800 ? 2.8 : width < 2900 ? 2.9 : width < 3000 ? 3.0 : width < 3100 ? 3.1 : width < 3200 ? 3.2 : width < 3300 ? 3.3 : width < 3400 ? 3.4 : width < 3500 ? 3.5 : width < 3600 ? 3.6 : width < 3700 ? 3.7 : width < 3800 ? 3.8 : width < 3900 ? 3.9 : width < 4000 ? 4.0 : width < 4100 ? 4.1 : width < 4200 ? 4.2 : width < 4300 ? 4.3 : width < 4400 ? 4.4 : width < 4500 ? 4.5 : width < 4600 ? 4.6 : width < 4700 ? 4.7 : width < 4800 ? 4.8 : width < 4900 ? 4.9 : width < 5000 ? 5.0 : width < 5100 ? 5.1 : width < 5200 ? 5.2 : width < 5300 ? 5.3 : width < 5400 ? 5.4 : width < 5500 ? 5.5 : width < 5600 ? 5.6 : majwid / 1116)
.attr("stroke", "black")
.style("cursor", "pointer")
.call(d3.drag().on("drag", handleTimeCircleDrag));
updateControlPositions();
}
fontSize = 14;
getSolarAngle = (date) => (dayOfYear(date) + 10) / 365 * Math.PI * 2 - Math.PI / 2;
/*
* Formulas uses the CBM model as reviewed here:
* https://www.ikhebeenvraag.be/mediastorage/FSDocument/171/Forsythe+-+A+model+comparison+for+daylength+as+a+function+of+latitude+and+day+of+year+-+1995.pdf
*/
dayLength = (date, latitude) => {
const yearStart = new Date(date.getFullYear(), 0, 1);
const dayOfYear = Math.floor((date.getTime() - yearStart.getTime())/86400000) + 1;
const revAngle = 0.2163108 + 2 * Math.atan(0.9671396 * Math.tan(0.00860 * (dayOfYear - 186)));
const decAngle = Math.asin(0.39795 * Math.cos(revAngle));
/* daylight coefficient selected for apparent sunrise/sunset */
const p = 0.8333
const intResult =
(Math.sin((p * Math.PI) / 180) +
Math.sin((latitude * Math.PI) / 180) * Math.sin(decAngle)) /
(Math.cos((latitude * Math.PI) / 180) * Math.cos(decAngle));
if (intResult >= 1) return 24;
if (intResult <= -1) return 0;
return 24 - 24 * Math.acos(intResult) / Math.PI;
}
yearDates = (year) => {
const startDate = new Date(year, 0, 1+60);
const endDate = new Date(year + 1, 0, 1+60);
return d3.timeDay.range(startDate, endDate);
}
height = {
const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, sphere)).bounds(sphere);
const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
return dy;
}
sphere = ({type: "Sphere"})
selectedProjection = select ? select.value() : d3.geoEquirectangular()
projection = {
let proj = selectedProjection;
if (proj.rotate) proj.rotate([-turn2long(yaw), -turn2degr(pitch), turn2degr(roll)]);
return proj;
}
// https://observablehq.com/@fil/d3-projections
projections = [
{ name: "Airocean", value: d3.geoAirocean },
{ name: "Airy’s minimum error", value: d3.geoAiry },
{ name: "Aitoff", value: d3.geoAitoff },
{ name: "American polyconic", value: d3.geoPolyconic },
{ name: "Armadillo", value: d3.geoArmadillo, options: { clip: { type: "Sphere" } } },
{ name: "August", value: d3.geoAugust },
{ name: "azimuthal equal-area", value: d3.geoAzimuthalEqualArea },
{ name: "azimuthal equidistant", value: d3.geoAzimuthalEquidistant },
{ name: "Baker dinomic", value: d3.geoBaker },
{ name: "Berghaus’ star", value: d3.geoBerghaus, options: { clip: { type: "Sphere" } } },
{ name: "Bertin’s 1953", value: d3.geoBertin1953 },
{ name: "Boggs’ eumorphic", value: d3.geoBoggs },
{ name: "Boggs’ eumorphic (interrupted)", value: d3.geoInterruptedBoggs, options: { clip: { type: "Sphere" } } },
{ name: "Bonne", value: d3.geoBonne },
{ name: "Bottomley", value: d3.geoBottomley },
{ name: "Bromley", value: d3.geoBromley },
{ name: "Butterfly (gnomonic)", value: d3.geoPolyhedralButterfly },
{ name: "Butterfly (Collignon)", value: d3.geoPolyhedralCollignon },
{ name: "Butterfly (Waterman)", value: d3.geoPolyhedralWaterman },
{ name: "Cahill-Keyes", value: d3.geoCahillKeyes },
{ name: "Collignon", value: d3.geoCollignon },
{ name: "conic equal-area", value: d3.geoConicEqualArea },
{ name: "conic equidistant", value: d3.geoConicEquidistant },
{ name: "Craig retroazimuthal", value: d3.geoCraig },
{ name: "Craster parabolic", value: d3.geoCraster },
{ name: "Cox", value: d3.geoCox },
{ name: "cubic", value: d3.geoCubic },
{ name: "cylindrical equal-area", value: d3.geoCylindricalEqualArea },
{ name: "cylindrical stereographic", value: d3.geoCylindricalStereographic },
{ name: "dodecahedral", value: d3.geoDodecahedral },
{ name: "Eckert I", value: d3.geoEckert1 },
{ name: "Eckert II", value: d3.geoEckert2 },
{ name: "Eckert III", value: d3.geoEckert3 },
{ name: "Eckert IV", value: d3.geoEckert4 },
{ name: "Eckert V", value: d3.geoEckert5 },
{ name: "Eckert VI", value: d3.geoEckert6 },
{ name: "Eisenlohr conformal", value: d3.geoEisenlohr },
{ name: "Equal Earth", value: d3.geoEqualEarth },
{ name: "Equirectangular (plate carrée)", value: d3.geoEquirectangular },
{ name: "Fahey pseudocylindrical", value: d3.geoFahey },
{ name: "flat-polar parabolic", value: d3.geoMtFlatPolarParabolic },
{ name: "flat-polar quartic", value: d3.geoMtFlatPolarQuartic },
{ name: "flat-polar sinusoidal", value: d3.geoMtFlatPolarSinusoidal },
{ name: "Foucaut’s stereographic equivalent", value: d3.geoFoucaut },
{ name: "Foucaut’s sinusoidal", value: d3.geoFoucautSinusoidal },
{ name: "general perspective", value: d3.geoSatellite },
{ name: "Gingery", value: d3.geoGingery, options: { clip: { type: "Sphere" } } },
{ name: "Ginzburg V", value: d3.geoGinzburg5 },
{ name: "Ginzburg VI", value: d3.geoGinzburg6 },
{ name: "Ginzburg VIII", value: d3.geoGinzburg8 },
{ name: "Ginzburg IX", value: d3.geoGinzburg9 },
{ name: "Goode’s homolosine", value: d3.geoHomolosine},
{ name: "Goode’s homolosine (interrupted)", value: d3.geoInterruptedHomolosine, options: { clip: { type: "Sphere" } } },
{ name: "gnomonic", value: d3.geoGnomonic },
{ name: "Gringorten square", value: d3.geoGringorten },
{ name: "Gringorten quincuncial", value: d3.geoGringortenQuincuncial },
{ name: "Guyou square", value: d3.geoGuyou },
{ name: "Hammer", value: d3.geoHammer },
{ name: "Hammer retroazimuthal", value: d3.geoHammerRetroazimuthal, options: { clip: { type: "Sphere" } } },
{ name: "HEALPix", value: d3.geoHealpix, options: { clip: { type: "Sphere" } } },
{ name: "Hill eucyclic", value: d3.geoHill },
{ name: "Hufnagel pseudocylindrical", value: d3.geoHufnagel },
{ name: "icosahedral", value: d3.geoIcosahedral },
{ name: "Imago", value: d3.geoImago },
{ name: "Kavrayskiy VII", value: d3.geoKavrayskiy7 },
{ name: "Lagrange conformal", value: d3.geoLagrange },
{ name: "Larrivée", value: d3.geoLarrivee },
{ name: "Laskowski tri-optimal", value: d3.geoLaskowski },
{ name: "Loximuthal", value: d3.geoLoximuthal },
{ name: "Mercator", value: d3.geoMercator },
{ name: "Miller cylindrical", value: d3.geoMiller },
{ name: "Mollweide", value: d3.geoMollweide },
{ name: "Mollweide (Goode’s interrupted)", value: d3.geoInterruptedMollweide, options: { clip: { type: "Sphere" } } },
{ name: "Mollweide (interrupted hemispheres)", value: d3.geoInterruptedMollweideHemispheres, options: { clip: { type: "Sphere" } } },
{ name: "Natural Earth", value: d3.geoNaturalEarth1 },
{ name: "Natural Earth II", value: d3.geoNaturalEarth2 },
{ name: "Nell–Hammer", value: d3.geoNellHammer },
{ name: "Nicolosi globular", value: d3.geoNicolosi },
{ name: "orthographic", value: d3.geoOrthographic },
{ name: "Patterson cylindrical", value: d3.geoPatterson },
{ name: "Peirce quincuncial", value: d3.geoPeirceQuincuncial },
{ name: "rectangular polyconic", value: d3.geoRectangularPolyconic },
{ name: "Robinson", value: d3.geoRobinson },
{ name: "sinusoidal", value: d3.geoSinusoidal },
{ name: "sinusoidal (interrupted)", value: d3.geoInterruptedSinusoidal, options: { clip: { type: "Sphere" } } },
{ name: "sinu-Mollweide", value: d3.geoSinuMollweide },
{ name: "sinu-Mollweide (interrupted)", value: d3.geoInterruptedSinuMollweide, options: { clip: { type: "Sphere" } } },
{ name: "stereographic", value: d3.geoStereographic },
{ name: "Lee’s tetrahedal", value: d3.geoTetrahedralLee },
{ name: "Times", value: d3.geoTimes },
{ name: "Tobler hyperelliptical", value: d3.geoHyperelliptical },
{ name: "transverse Mercator", value: d3.geoTransverseMercator },
{ name: "Van der Grinten", value: d3.geoVanDerGrinten },
{ name: "Van der Grinten II", value: d3.geoVanDerGrinten2 },
{ name: "Van der Grinten III", value: d3.geoVanDerGrinten3 },
{ name: "Van der Grinten IV", value: d3.geoVanDerGrinten4 },
{ name: "Wagner IV", value: d3.geoWagner4 },
{ name: "Wagner VI", value: d3.geoWagner6 },
{ name: "Wagner VII", value: d3.geoWagner7 },
{ name: "Werner", value: d3.geoBonne ? () => d3.geoBonne().parallel(90) : null },
{ name: "Wiechel", value: d3.geoWiechel },
{ name: "Winkel tripel", value: d3.geoWinkel3 }
]
EventType = ({
LocationChange: "LOCATION_CHANGE",
DateHourChange: "DATE_HOUR_CHANGE"
})
mapcolors = ({
night: "#719fb6",
day: "#ffe438",
grid: "#4b6a79",
ocean: "#adeeff",
land: "#90ff7888",
sun: "#ffb438"
})
toRadians = (val) => val * Math.PI / 180
toDegrees = (val) => val * 180 / Math.PI;
land = topojson.feature(world, world.objects.land);
d3 = require("d3@7", "d3-geo-projection@3")
function input(config) {
let {
form,
type = "text",
attributes = {},
action,
getValue,
title,
description,
format,
display,
submit,
options
} = config;
const wrapper = html`<div></div>`;
if (!form)
form = html`<form>
<input name=input type=${type} />
</form>`;
Object.keys(attributes).forEach(key => {
const val = attributes[key];
if (val != null) form.input.setAttribute(key, val);
});
if (submit)
form.append(
html`<input name=submit type=submit style="margin: 0 0.75em" value="${
typeof submit == "string" ? submit : "Submit"
}" />`
);
form.append(
html`<output name=output style="font: 14px Menlo, Consolas, monospace; margin-left: 0.1em; text-align:center;"></output>`
);
if (title)
form.prepend(
html`<div style="font: 700 0.9rem sans-serif; margin-bottom: 3px;">${title}</div>`
);
if (description)
form.append(
html`<div style="font-size: 0.85rem; font-style: italic; margin-top: 3px;">${description}</div>`
);
if (format)
format = typeof format === "function" ? format : d3format.format(format);
if (action) {
action(form);
} else {
const verb = submit
? "onsubmit"
: type == "button"
? "onclick"
: type == "checkbox" || type == "radio"
? "onchange"
: "oninput";
form[verb] = e => {
e && e.preventDefault();
const value = getValue ? getValue(form.input) : form.input.value;
if (form.output) {
const out = display ? display(value) : format ? format(value) : value;
if (out instanceof window.Element) {
while (form.output.hasChildNodes()) {
form.output.removeChild(form.output.lastChild);
}
form.output.append(out);
} else {
form.output.value = out;
}
}
form.value = value;
if (verb !== "oninput")
form.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};
if (verb !== "oninput")
wrapper.oninput = e => e && e.stopPropagation() && e.preventDefault();
if (verb !== "onsubmit") form.onsubmit = e => e && e.preventDefault();
form[verb]();
}
while (form.childNodes.length) {
wrapper.appendChild(form.childNodes[0]);
}
form.append(wrapper);
return form;
}
d3format = require("d3-format@1")
// https://observablehq.com/@enjalot/draggable-world-map-coordinates-input
function worldMapCoordinates(config = {}, dimensions) {
const {
value = [], title, description, width = dimensions[0]
} = Array.isArray(config) ? {value: config[0]} : config;
const height = dimensions[1];
let [lon, lat] = value;
lon = lon != null ? lon : null;
lat = lat != null ? lat : null;
const formEl = html`<form id="formEl" style="width: ${width}px;"></form>`;
const context = DOM.context2d(width, height);
const canvas = context.canvas;
canvas.style.margin = `10px 0px 0px 0px`;
const projection = config[1]
.precision(0.1)
.fitSize([width, height], { type: "Sphere" });
const path = d3.geoPath(projection, context).pointRadius(2.5);
formEl.append(canvas);
function fillMesh(f) {
context.beginPath();
path(f);
context.fillStyle = zonecolor(f.properties.zone);
context.fill();
context.innerHTML = `<title>${f.properties.places} ${f.properties.time_zone}</title>`;
}
function draw() {
if (!utctoggle) {
context.beginPath(); path({type: "Sphere"});
context.fillStyle = window.darkmode ? "#007FFF" : mapcolors.ocean;
context.fill();
if (gridtoggle) {
deczones.map((f, i) => {
context.beginPath();
path(f);
context.fillStyle = hsla10[i];
context.fill();
})
}
}
if (utctoggle) {
zones.map(f => fillMesh(f))
}
context.beginPath();
path(land);
if (!utctoggle) {
context.fillStyle = window.darkmode ? "#0808" : mapcolors.land;
context.fill();
}
context.strokeStyle = `#000`;
context.stroke();
if (bordertoggle) {
context.beginPath();
path(borders);
context.lineWidth = 1.25;
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.stroke();
}
if (utctoggle) {
context.beginPath();
path(mesh);
context.lineWidth = 1.25;
context.strokeStyle = `#999`;
context.stroke();
}
if (gridtoggle) {
context.beginPath();
path(graticule);
context.lineWidth = 1.25;
context.strokeStyle = utctoggle || !window.darkmode ? "#000" : "#fff";
context.stroke();
context.fillStyle = "#000";
context.font = `${width < 769 ? 14 : width < 1000 ? 16 : width < 1500 ? 18 : width < 2000 ? 20 : width < 3000 ? 30 : width < 4000 ? 40 : width < 5000 ? 50 : width < 6000 ? 60 : 80}px serif`;
d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, 54.7])));
d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, -59.7])));
// context.font = width < 760 ? "12px serif" : "21px serif";
// context.fillStyle = `#000`;
// d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, 27.5])));
// d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, -48])));
// d3.range(-18, 336 + 1, 36).map(x => context.fillText(formatLongitude(x), ...projection([x, 90])));
// d3.range(-18, 336 + 1, 36).map(x => context.fillText(formatLongitude(x), ...projection([x, -90])));
}
if (suntoggle) {
context.beginPath();
path(night);
context.fillStyle = "rgba(0,0,255,0.3)";
context.fill();
context.beginPath();
path.pointRadius(width / 84 + 5);
path({type: "Point", coordinates: sun});
context.strokeStyle = "#0009";
context.fillStyle = "#ff0b";
context.lineWidth = 1;
context.stroke();
context.fill();
}
if (lon != null && lat != null) {
path.pointRadius(width < 2000 ? 17 : width < 3000 ? 34 : width < 4000 ? 51 : width < 5000 ? 68 : 85); context.strokeStyle = "black";
context.beginPath(); path({type: "Point", coordinates: [lon, lat]}); context.lineWidth = 1; context.stroke();
context.lineWidth = width < 2000 ? 6 : width < 3000 ? 12 : width < 4000 ? 18 : width < 5000 ? 24 : 30;
path.pointRadius(width < 2000 ? 14 : width < 3000 ? 28 : width < 4000 ? 42 : width < 5000 ? 56 : 70); context.strokeStyle = "red";
context.beginPath(); path({type: "Point", coordinates: [lon, lat]}); context.stroke();
context.lineWidth = width < 1000 ? 1 : width < 2000 ? 2 : width < 3000 ? 3 : width < 4000 ? 4 : width < 5000 ? 5 : 6;
}
table.rows[1].cells[0].innerHTML = createCellDiv(long2turn(lon), 10, "positive")
table.rows[1].cells[1].innerHTML = createCellDiv(lati2turn(lat), 2.5, lat < 0 ? "negative" : "positive")
context.lineWidth = 1;
}
let drag = d3.drag()
.on("drag", (event) => {
let coords = projection.invert([event.x, event.y]);
lon = +coords[0].toFixed(2);
lat = +coords[1].toFixed(2);
draw();
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
})
d3.select(canvas).call(drag)
canvas.onclick = function(ev) {
const { offsetX, offsetY } = ev;
let coords = projection.invert([offsetX, offsetY]);
lon = +coords[0].toFixed(2);
lat = +coords[1].toFixed(2);
draw();
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};
draw();
function resetlatlon() {
lon = 162;
lat = 0;
set(viewof bordertoggle, false);
set(viewof gridtoggle, false);
set(viewof suntoggle, false);
set(viewof utctoggle, false);
set(viewof yaw, 500);
set(viewof pitch, 0);
set(viewof roll, 0);
set(viewof select, projections.find(t => t.name === "Equirectangular (plate carrée)"));
draw();
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}
table.onkeyup = function(ev) {
if ([
// https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes#heading-a-full-list-of-key-event-values
8, 9, 13, 27, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 109, 189
].includes(ev.which)) {
const newLon = parseInt(liveTable[0].Milliparallel)
const newLat = parseInt(liveTable[0].Millimeridian)
lon = newLon != null || !isNaN(newLon) ? turn2long(newLon) : lon;
lat = newLat != null || !isNaN(newLat) ? turn2lati(newLat) : lat;
table.rows[1].cells[0].innerHTML = createCellDiv(long2turn(lon), 10, "positive")
table.rows[1].cells[1].innerHTML = createCellDiv(lati2turn(lat), 2.5, lat < 0 ? "negative" : "positive")
draw();
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}
}
rstbtn.on('click', resetlatlon);
const form = input({
type: "worldMapCoordinates",
title,
description,
display: v => "",
// display: v => (width > 300) ? html`<div style="width: ${width}px; pointer-events: none; white-space: nowrap; color: window.darkmode ? #fff : #000; text-align: center; font: ${width > 1000 ? width / 50 : width / 40}px monospace; position: relative; top: ${-28 - width / 25}px; margin-bottom: -.4em;">
// <span style="color: window.darkmode ? #fff : #000;">Zone:</span> ${lon != null ? long2zone(lon) : ""}
//
// <span style="color: window.darkmode ? #fff : #000;">Longitude:</span> ${lon != null ? (long2turn(lon)).toFixed(0) : ""}
//
// <span style="color: window.darkmode ? #fff : #000;">Latitude:</span> ${lat != null ? (lati2turn(lat)).toFixed(0) : ""}
// </div>` : '',
getValue: () => [lon != null ? lon : null, lat != null ? lat : null],
form: formEl
});
return form;
}
window.darkmode = document.getElementsByTagName("body")[0].className.match(/quarto-dark/) ? true : false;document.getElementsByClassName("quarto-color-scheme-toggle")[0].onclick = function (e) {
window.quartoToggleColorScheme();
window.darkmode = document.getElementsByTagName("body")[0].className.match(/quarto-dark/) ? true : false;
worldMapCoordinates.draw();
app.drawEarth();
return false;
};sun = {
const now = new Date;
const day = new Date(+now).setUTCHours(0, 0, 0, 0);
const t = solar.century(now);
const longitude = (day - now) / 864e5 * 360 - 180;
return [longitude - solar.equationOfTime(t) / 4, solar.declination(t)];
}
night = d3.geoCircle()
.radius(90)
.center(antipode(sun))
()
antipode = ([longitude, latitude]) => [longitude + 180, -latitude]
solar = require("solar-calculator@0.3/dist/solar-calculator.min.js")
viewof fancySecondsOFF = Inputs.toggle({
label: "Ticking clock",
value: true,
})
function setStyle(content, style = {}) {
function yiq(color) {
const {r, g, b} = d3.rgb(color);
return (r * 299 + g * 587 + b * 114) / 1000 / 255; // returns values between 0 and 1
}
const {
background,
color = yiq(background) >= 0.6 ? "#111" : "white",
padding = "0 1px",
borderRadius = "4px",
fontWeight = 900,
fontSize = "1em",
...rest
} = typeof style === "string" ? {background: style} : style;
return htl.html`<span style=${{
background,
color,
padding,
borderRadius,
fontWeight,
...rest
}}>${content}</span>`;
}
function yiq(color) {
const {r, g, b} = d3.rgb(color);
return (r * 299 + g * 587 + b * 114) / 1000 / 255; // returns values between 0 and 1
}
function textcolor(content, style = {}) {
const {
background,
color = yiq(background) > 0.51 ? "#000" : "white",
padding = "0 5px",
borderRadius = "4px",
fontWeight = 400,
fontFamily = "monospace",
...rest
} = typeof style === "string" ? {background: style} : style;
return htl.html`<span style=${{
background,
color,
padding,
borderRadius,
fontWeight,
fontFamily,
...rest
}}>${content}</span>`;
}
function isLeapYear(y) {
y += 1;
return y % 4 == 0 && y % 100 != 0 || y % 400 == 0;
}
elapsed = {
let i = 0;
while (true) {
yield Promises.tick(864, ++i);
}
}
piecewiseColor = d3.piecewise(d3.interpolateRgb, [
"#f00", // 0 0 red
"#f50", // 0.25 20 yr
"#f60", // 0.5 24 yr orangered
"#f70", // 0.75 28 yr
"#f90", // 1 36 yr orange
"#fb0", // 1.25 44 yr
"#fc0", // 1.5 48 yr yelloworange
"#fd0", // 1.75 52 yr
"#ff0", // 2 60 yellow
"#ef0", // 2.25 64 gy
"#df0", // 2.5 68 gy limeyellow
"#cf0", // 2.75 72 gy
"#af0", // 3 80 gy lime
"#8f0", // 3.25 88 gy
"#7f0", // 3.5 92 gy greenlime
"#6f0", // 3.75 96 gy
"#0f0", // 4 120 green
"#0f7", // 4.25 148 cg
"#0f9", // 4.5 156 cg cyangreen
"#0fb", // 4.75 164 cg
"#0ff", // 5 180 cyan
"#0cf", // 5.25 192 bc
"#0bf", // 5.5 196 bc azurecyan
"#0af", // 5.75 200 bc
"#08f", // 6 208 bc azure
"#06f", // 6.25 216 bc
"#05f", // 6.5 220 bc blueazure
"#04f", // 6.75 224 bc
"#00f", // 7 240 blue
"#50f", // 7.25 260 mb
"#60f", // 7.5 264 mb purpleblue
"#70f", // 7.75 268 mb
"#90f", // 8 276 mb purple
"#b0f", // 8.25 284 mb
"#c0f", // 8.5 288 mb violetpurple
"#d0f", // 8.75 292 mb
"#f0f", // 9 300 magenta
"#f0a", // 9.25 320 rm
"#f08", // 9.5 328 rm
"#f06", // 9.75 336 rm
"#f00", // 0 0 red
])
slStr = `, 100%, 50%)`
elaTime = elapsed % 1e5
elaTimeHsl = textcolor(elaTime, `hsl(${d3.hsl(piecewiseColor(elaTime % 1000 / 1000)).h}` + slStr)
loadDote = unix2dote(Date.now(), 0)
loadydz = dote2date(...loadDote)
loadYear = loadydz[0]
loadYearHsl = textcolor(loadYear, d3.color(piecewiseColor(loadYear % 1000 / 1000)).formatHex())
loadAda = loadydz[1]
loadIsLeap = isLeapYear(loadYear)
loadAdaHsl = textcolor(loadAda.toFixed(5), d3.color(piecewiseColor(loadAda / (365 + loadIsLeap))).formatHex())
decDate = Math.floor(ydz[1])
decMoty = Math.floor((5 * decDate + 2) / 153)
isoYear = decYear + (decMoty > 9)
month = decMoty < 10 ? decMoty + 3 : decMoty - 9
decHour = decTime * 24
decMinute = (decHour % 1) * 60
decSecond = (decMinute % 1) * 60
isoHour = Math.floor(decHour)
isoMinute = Math.floor(decMinute)
isoSecond = Math.floor(decSecond)
decDek = Math.floor(decDate / 10)
decDod = decDate % 10
decDotm = Math.floor(decDate - (153 * decMoty + 2) / 5 + 1)
selDote = unix2dote(selectedDate.getTime() - 86400000, 0)
selDate = Math.floor(dote2date(...selDote)[1])
selTime = selectedHour / 24
selTimeOne = selTime % 1
selTimeTen = selTime * 10 % 10
selTimeMil = selTimeOne.toFixed(3).slice(2)
selTimeCmd = selTimeOne.toFixed(5).slice(2)
selAda = selDate + selTime
selLati = lati2turn(location[1])
decDodHsl = textcolor(decDod, d3.color(piecewiseColor(decDod / 10)).formatHex())
decDateHsl = textcolor(decDate.toString().padStart(3, "0"), d3.color(piecewiseColor(decDate / (365 + isLeapYear(decYear)))).formatHex())
decTimeHsl = textcolor(decTime, d3.color(piecewiseColor(decTime / 10)).formatHex())
selDateHsl = textcolor(selDate.toString().padStart(3, "0"), d3.color(piecewiseColor(selDate / 365)).formatHex())
selLatiHsl = textcolor(selLati.toFixed(0), d3.color(piecewiseColor((selLati + 250) % 250 / 250)).formatHex())
selTimeDay = textcolor(selTimeMil, d3.color(piecewiseColor(selTime)).formatHex())
selTimeHsl0 = textcolor(selTimeTen.toFixed(4), d3.color(piecewiseColor(selTime)).formatHex())
selTimeHsl1 = textcolor(selTimeTen.toFixed(4), d3.color(piecewiseColor(selTime)).formatHex())
selZoneHsl = textcolor(selectedZone, d3.color(piecewiseColor(selectedZone / 10)).formatHex())
selAdaDay = textcolor(selAda.toFixed(3).padStart(6, "0"), d3.color(piecewiseColor(selAda / 365)).formatHex())
selAdaDec = textcolor((selAda * 10).toFixed(4).padStart(9, "0"), d3.color(piecewiseColor(selAda / 365)).formatHex())
decZoneHsl = textcolor(selectedZone, d3.color(piecewiseColor(selectedZone / 10)).formatHex())
viewof selectedDate = Inputs.input(new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().getHours()))
viewof selectedHour = Inputs.input(new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().getHours()).getHours())
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input", {bubbles: true}));
}
hD121 = d3.hsl(piecewiseColor(121 / 365)).h
hD268 = d3.hsl(piecewiseColor(268 / 365)).h
hD305 = d3.hsl(piecewiseColor(305 / 365)).h
hD306 = d3.hsl(piecewiseColor(306 / 365)).h
h1by320 = d3.hsl(piecewiseColor(1 / 320)).h
h1by8640 = d3.hsl(piecewiseColor(1 / 8640)).h
hues = Object.fromEntries([
.0083,
.0166,
.0229,
.025,
.287,
.0333,
.0416,
.05,
.125,
.333,
.375,
.429,
.533,
.969,
.999,
].map(i => [i, d3.hsl(piecewiseColor(i)).h])
);
// https://observablehq.com/@mattdzugan/population-by-time-zone-creating-a-dataset
populationByTimeZone = FileAttachment("../../asset/populationByTimeZone.json").json();
populationHemisphere = FileAttachment("../../asset/populationByHemisphere.json").json();
sortedPop = populationByTimeZone.sort(
(a, b) => sortParams[1] ? sortFunc(a.number, b.number) : sortFunc(a.pop, b.pop)
)
sortFunc = sortParams[0] ? d3.ascending : d3.descending
sortedPopHemi = populationHemisphere.sort(
(a, b) => sortParamsHemi[1] ? sortFuncHemi(a.Offset, b.Offset) : sortFuncHemi(a.Population, b.Population)
)
sortFuncHemi = sortParamsHemi[0] ? d3.ascending : d3.descending
popBySign = d3.rollup(sortedPop, v => d3.sum(v, d => d.pop / 1e9), d => d.Sign)
totalPop = d3.sum(sortedPop, d => d.pop / 1e9)
utcOffsetM = -(new Date).getTimezoneOffset()
utcOffsetD = utcOffsetM / 144
utcOffsetP = (utcOffsetD + 10) % 10
decZone = ydz[2]
decZonePos = (decZone + 10) % 10
utcOffDiff = parseFloat((Math.round(utcOffsetD) - utcOffsetD).toFixed(2))
utcOffHslM = textcolor(utcOffsetM, `hsl(${d3.hsl(piecewiseColor(utcOffsetP / 10)).h}` + slStr)
utcOffHslD = textcolor(parseFloat(utcOffsetD.toFixed(2)), `hsl(${d3.hsl(piecewiseColor(utcOffsetP / 10)).h}` + slStr)
decZonHslP = textcolor(decZonePos, `hsl(${d3.hsl(piecewiseColor(decZonePos / 10)).h}` + slStr)
utcOffsetMdiffHsl = textcolor(parseFloat((utcOffDiff * 144).toFixed(2)), `hsl(${d3.hsl(piecewiseColor(utcOffDiff / 10)).h}` + slStr)
utcOffDiffHsl = textcolor(utcOffDiff, `hsl(${d3.hsl(piecewiseColor(utcOffDiff / 10)).h}` + slStr)
function date2dote(year = 1969, doty = 306, zone = 0) {
const cote = Math.floor((year >= 0 ? year : year - 399) / 400), yote = year - cote * 400;
return [cote * 146097 + yote * 365 + Math.floor(yote / 4) - Math.floor(yote / 100) + doty, zone]
}
leapSecondDotes = [
[1972, 121],
[1972, 305],
[1973, 305],
[1974, 305],
[1975, 305],
[1976, 305],
[1977, 305],
[1978, 305],
[1979, 305],
[1981, 121],
[1982, 121],
[1983, 121],
[1985, 121],
[1987, 305],
[1989, 305],
[1990, 305],
[1992, 121],
[1993, 121],
[1994, 121],
[1995, 305],
[1997, 121],
[1998, 305],
[2005, 305],
[2008, 305],
[2012, 121],
[2015, 121],
[2016, 305],
].map(x => date2dote(...x)[0])
leapSecondDote = date2dote(leapSecondYear, leapSecondDate)[0]
leapCount = leapSecondDotes.filter(dote => leapSecondDote > dote).length;
leapColor = d3.color(piecewiseColor(leapCount / 86400)).formatHex()
leapCountHsl = textcolor(leapCount, leapColor)
frac = require("fraction.js")
leapFrac = frac(leapCount).div(8640).toFraction()
leapTzoHsl0 = textcolor(leapFrac, leapColor)
leapTzoHsl1 = textcolor(leapFrac, leapColor)
leapTzoHsl2 = textcolor(leapFrac, leapColor)
hmsTzo = frac(hmsinput[0]).div(24).add(hmsinput[1], 1440).add(hmsinput[2], 86400).mod(1)
hmsTzox10 = hmsTzo.mul(10)
pluralx10 = hmsTzox10.equals(1) ? "" : "s"
hmsRounded = hmsTzox10.round(digits - 1)
hmsRef = hmsRounded.sub(hmsTzox10)
hmsRefHsl = textcolor(hmsRef.abs().toFraction(), d3.color(piecewiseColor(Number(hmsRef.div(10)))).formatHex())
hmsRefSign = hmsRef.lt(0) ? "-" : "+"
hmsTzoHsl = textcolor(hmsTzox10.toFraction(), d3.color(piecewiseColor(Number(hmsTzo))).formatHex())
hmsRodHsl = textcolor(hmsRounded, d3.color(piecewiseColor(Number(hmsRounded.div(10)))).formatHex())
hmsTod = (zeroTime + hmsTzo) % 1
hmsTodHsl = textcolor((hmsTod * 10).toFixed(4), d3.color(piecewiseColor(hmsTod)).formatHex())
leapTod = (zeroTime + leapCount / 86400) % 1
leapDecidayTod = (leapTod * 10).toFixed(4)
leapTodHsl0 = textcolor(leapDecidayTod, d3.color(piecewiseColor(leapTod)).formatHex())
leapTodHsl1 = textcolor(leapDecidayTod, d3.color(piecewiseColor(leapTod)).formatHex())
longInputHsl = textcolor(lonInput, d3.color(piecewiseColor(lonInput / 10)).formatHex())
eot = getEot(leapSecondDate)
astTzo = eot + lonInput
astTzoHsl = textcolor(astTzo.toFixed(4), d3.color(piecewiseColor(astTzo / 10)).formatHex())
eotHsl = textcolor(eot.toFixed(4), d3.color(piecewiseColor(eot / 10)).formatHex())
eotSum = (zeroTime + astTzo + 10) % 10
eotSumHsl = textcolor(eotSum.toFixed(4), d3.color(piecewiseColor(eotSum / 10)).formatHex())
// https://observablehq.com/@mcmcclur/adaptive-plotter
function build_samples(f, a, b, opts = {}) {
let { N = 9, max_depth = 6 } = opts;
let dx = (b - a) / N;
let root_intervals = Array.from({ length: N }).map(
(_, i) => new Interval(a + i * dx, a + (i + 1) * dx, 0)
);
root_intervals.forEach((I) => {
I.fa = f(I.a);
I.fb = f(I.b);
});
root_intervals.reverse();
let stack = root_intervals;
let cnt = 0;
let pts = [];
let nodeRight, nodeLeft;
while (stack.length > 0 && cnt++ < 100000) {
let node = stack.pop();
if (test(f, node, opts)) {
let midpoint = node.midpoint;
let new_depth = node.depth + 1;
if (new_depth <= max_depth) {
let a_left = node.a;
let b_left = midpoint;
nodeLeft = new Interval(a_left, b_left, new_depth);
nodeLeft.fa = f(a_left);
nodeLeft.fb = f(b_left);
node.left = nodeLeft;
let a_right = midpoint;
let b_right = node.b;
nodeRight = new Interval(a_right, b_right, new_depth);
nodeRight.fa = f(a_left);
nodeRight.fb = f(b_left);
node.right = nodeRight;
stack.push(nodeRight);
stack.push(nodeLeft);
} else {
pts.push(node.a);
}
} else {
pts.push(node.a);
}
}
pts.push(b);
// pts = pts.map(x => ({ x: x, y: f(x) }));
pts = pts.map((x) => [x, f(x)]);
if (opts.show_roots) {
let function_roots = [];
pts.forEach(function (o, i) {
if (i < pts.length - 1 && Math.sign(o.y) != Math.sign(pts[i + 1].y)) {
function_roots.push((o.x + pts[i + 1].x) / 2);
}
});
pts.function_roots = function_roots;
}
return pts;
}
function test(f, I, opts = {}) {
let { angle_tolerance = 0.01, check_roots = false } = opts;
let a = I.a;
let b = I.b;
let dx2 = (b - a) / 2;
let m = (a + b) / 2;
let fm = f(m);
I.midpoint = m;
I.f_mid = fm;
if (check_roots && Math.sign(I.fa) != Math.sign(I.fb)) {
return true;
}
let alpha = Math.atan((I.f_mid - I.fa) / dx2);
let beta = Math.atan((I.fb - I.f_mid) / dx2);
return Math.abs(alpha - beta) > angle_tolerance;
}
class Interval {
constructor(a, b, depth) {
this.a = a;
this.b = b;
this.depth = depth;
}
}
colors = ({
night: "#719fb6",
day: "#ffe438",
grid: "#4b6a79",
ocean: "lightblue",
land: "#f5f1dc",
sun: "#ffe438"
})
// https://observablehq.com/@parlant/editable-table
function createTable(data, options) {
let table = html`<table class="editable-table"></table>`;
table.innerHTML = xss.filterXSS(tableify.default(data));
makeTableEditable(table, options);
return table;
}
table.setAttribute("class", "table")tableify = import("https://cdn.skypack.dev/tableify@1.1.1?min")
xss = import("https://cdn.skypack.dev/xss@1.0.14?min")
function createCellDiv(value, max, posorneg) {
return `<div class="${posorneg}" style="
width: ${Math.abs(value) / max}%;
float: left;
padding: 0px 0px 0px 2px;
text-indent: 2px;
box-sizing: border-box;
overflow: visible;
white-space: nowrap;
display: flex;
justify-content: start;">${Math.round(value)}</div>`
}
liveTable = observeTable(table)
function makeTableEditable(table, options) {
const defaults = {headerEditable: false, appendRows: true};
options = options === undefined ? {} : options;
for (let key in defaults) {
options[key] = options[key] === undefined ? defaults[key] : options[key];
}
return Generators.observe((_notify) => {
const navigate = (event) => {
const cell = event.target;
const row = cell.closest('tr');
const table = row.closest('table');
const isBody = row.parentNode.tagName === 'TBODY';
const isHeader = row.parentNode.tagName === 'THEAD';
const colIndex = cell.cellIndex;
const colCount = row.cells.length;
const rowIndex = row.rowIndex;
const rowCount = table.rows.length;
const headStop = options.headerEditable ? 0 : 1;
let direction = null;
let x = colIndex;
let y = rowIndex;
if (![
// https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes#heading-a-full-list-of-key-event-values
8, 9, 13, 16, 17, 18, 27, 33, 34, 35, 36, 37, 38, 39, 40, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 109, 189
].includes(event.which)) {
event.preventDefault();
}
else {
switch(event.code) {
// Tab cycles through the table, adding new rows as needed.
case 'Tab':
event.preventDefault();
if (event.altKey || event.shiftKey) {
direction = -1;
if (x - 1 < 0) {
if (y - 1 < headStop) break;
x = colCount - 1;
y = y - 1;
} else {
x = x - 1;
}
} else {
direction = 1;
if (x + 1 === colCount) {
x = 0;
y = y + 1;
} else {
x = x + 1;
}
}
break;
// Plain Enter navigates downwards.
// Shift + Enter or Alt + Enter goes up to the cell above.
case 'Enter':
event.preventDefault();
if (event.altKey || event.shiftKey) {
direction = -1;
x = x;
y = y - 1;
}
else {
direction = 1;
x = x;
y = y + 1;
}
break;
// The arrow keys allow you to navigate through cells.
// No new rows are added.
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
case 'Enter':
if (!event.altKey) break;
event.preventDefault();
switch(event.code) {
case 'ArrowUp':
direction = -1;
y = Math.max(y - 1, headStop);
break;
case 'ArrowDown':
direction = 1;
y = Math.min(y + 1, rowCount - 1);
break;
case 'ArrowLeft':
direction = -1;
x = Math.max(x - 1, 0);
break;
case 'ArrowRight':
direction = 1;
x = Math.min(x + 1, colCount - 1);
break;
}
break;
}
if (direction !== null) {
let nextRow;
if (y === rowCount) {
nextRow = options.appendRows ? addRowRelativeTo(row, direction) : row;
} else {
nextRow = table.rows[y];
}
let nextCell = nextRow.cells[x];
focusCell(nextCell);
}
};
}
table.addEventListener("keydown", navigate, false);
if (table.rows.length > 0) {
for (let row of table.rows) {
if (!options.headerEditable && row.rowIndex === 0) continue;
for (let cell of row.cells) {
let cellValue = cell.innerText
cell.innerHTML = `<div style="
width: ${Math.abs(cellValue) / (cell.cellIndex === 2 ? 2.5 : 10)}%;
float: left;
padding: 0px 0px 0px 2px;
text-indent: 2px;
box-sizing: border-box;
overflow: visible;
white-space: nowrap;
display: flex;
justify-content: start;">${cellValue}</div>`
if (cell.cellIndex === 3) continue;
cell.contentEditable = true;
}
}
}
return () => table.removeEventListener("keydown", navigate);
});
}
function observeTable(table) {
return Generators.observe((notify) => {
const keyinput = (event) => notify(parseTableData(table));
table.addEventListener("input", keyinput, false);
notify(parseTableData(table));
return () => window.removeEventListener("input", keyinput);
});
}
function parseTableData(table) {
const header = [];
const data = [];
for (let row of table.rows) {
const rowIndex = row.rowIndex;
const isHeader = row.parentNode.tagName === 'THEAD' && rowIndex === 0;
let obj = {};
for (let cell of row.cells) {
const head = header[cell.cellIndex];
if (isHeader) {
header.push(cell.innerText);
} else {
obj[head] = cell.innerText;
}
}
if (!isHeader) data.push(obj);
}
return JSON.parse(JSON.stringify(data));
}
function focusCell(td) {
const s = window.getSelection();
const r = document.createRange();
let textNode = td.childNodes[0];
const i = td.innerText.length;
td.focus();
if (textNode) {
r.setStart(textNode, i);
r.setEnd(textNode, i);
} else {
r.selectNode(td);
}
s.removeAllRanges();
s.addRange(r);
}
function addRowRelativeTo(tr, direction) {
const newTr = document.createElement('tr');
const insertPosition = direction == 1 ? 'afterend' : 'beforebegin';
tr.insertAdjacentElement(insertPosition, newTr);
for (let _td of Array.from(tr.children)) {
const newTd = document.createElement('td');
newTd.appendChild(document.createTextNode(''));
newTd.contentEditable = true;
newTr.appendChild(newTd);
}
return newTr;
}
// https://observablehq.com/@observablehq/text-color-annotations-in-markdown
rstbtn = d3.create('button').html('Reset').attr("id", "rstbtn").attr("class", "btn btn-quarto");
// https://observablehq.com/@recifs/add-a-class-to-an-observable-input--support
function labelToggle(inputType, inputLabel, inputValue, inputId) {
const input = inputType({label: inputLabel, value: inputValue});
input.setAttribute("id", inputId);
return input;
}
timezones = FileAttachment("../../asset/timezones.json").json()
zones = topojson.feature(timezones, timezones.objects.timezones).features
mesh = topojson.mesh(timezones, timezones.objects.timezones)
zonecolor = d3.scaleSequential(d3.interpolateRdBu).domain([-12, 14])
coor = [[[-18, -89.98], [-18, 89.98], [18, 89.98], [18, -89.98], [-18, -89.98], ]]
deczones = [...Array(10).keys()].map(
i => ({
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [coor[0].map(t => [t[0]+36*i, t[1]])]
},
"properties": []
})
)
hsla10 = [
`hsla(0, 100%, 50%, 100%)`, // red
`hsla(36, 100%, 50%, 100%)`, // orange
`hsla(60, 100%, 50%, 100%)`, // yellow
`hsla(80, 100%, 50%, 100%)`, // lime
`hsla(120, 100%, 50%, 100%)`, // green
`hsla(180, 100%, 50%, 100%)`, // cyan
`hsla(208, 100%, 50%, 100%)`, // azure
`hsla(240, 100%, 50%, 100%)`, // blue
`hsla(276, 100%, 50%, 100%)`, // violet
`hsla(300, 100%, 50%, 100%)`, // magenta
`hsla(0, 100%, 50%, 100%)`, // red
]
borders = topojson.mesh(countries, countries.objects.countries, (a, b) => a !== b)
countries = fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json").then(response => response.json())// https://observablehq.com/@d3/simple-clock
// https://observablehq.com/@drio/lets-build-an-analog-clock
posclock = {
const clockRadius = 200,
margin = 50,
w = (clockRadius + margin) * 2,
h = (clockRadius + margin) * 2,
hourHandLength = (2 * clockRadius) / 3,
minuteHandLength = clockRadius,
secondHandLength = clockRadius - 12,
secondHandBalance = 30,
secondTickStart = clockRadius,
secondTickLength = -10,
hourTickStart = clockRadius,
hourTickLength = -18,
secondLabelRadius = clockRadius + 16,
secondLabelYOffset = 5,
hourLabelRadius = clockRadius - 40,
hourLabelYOffset = 7,
radians = Math.PI / 180;
const ten = d3
.scaleLinear()
.range([0, 360])
.domain([0, 10]);
const sto = d3
.scaleLinear()
.range([0, 360])
.domain([0, 100]);
const handData = [
{
type: "hour",
value: 0,
length: -hourHandLength,
scale: ten
},
{
type: "minute",
value: 0,
length: -minuteHandLength,
scale: sto
},
{
type: "second",
value: 0,
length: -secondHandLength,
scale: sto,
balance: secondHandBalance
}
];
function drawClock() {
// create all the clock elements
updateData(); //draw them in the correct starting position
const face = svg
.append("g")
.attr("id", "clock-face")
.attr("transform", `translate(${[w / 2, h / 2]})`);
// add marks for seconds
face
.selectAll(".second-tick")
.data(d3.range(0, 100))
.enter()
.append("line")
.attr("class", "second-tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", secondTickStart)
.attr("y2", secondTickStart + secondTickLength)
.attr("transform", d => `rotate(${sto(d)})`);
// and labels...
face
.selectAll(".second-label")
.data(d3.range(0, 100, 5))
.enter()
.append("text")
.attr("class", "second-label")
.attr("text-anchor", "middle")
.attr("x", d => secondLabelRadius * Math.sin(sto(d) * radians))
.attr(
"y",
d =>
-secondLabelRadius * Math.cos(sto(d) * radians) + secondLabelYOffset
)
.text(d => d);
// ... and hours
face
.selectAll(".hour-tick")
.data(d3.range(0, 10, 1))
.enter()
.append("line")
.attr("class", "hour-tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", hourTickStart)
.attr("y2", hourTickStart + hourTickLength)
.attr("transform", d => `rotate(${ten(d)})`);
face
.selectAll(".hour-label")
.data(d3.range(0, 10, 1))
.enter()
.append("text")
.attr("class", "hour-label")
.attr("text-anchor", "middle")
.attr("x", d => hourLabelRadius * Math.sin(ten(d) * radians))
.attr(
"y",
d => -hourLabelRadius * Math.cos(ten(d) * radians) + hourLabelYOffset
)
.text(d => d);
const hands = face.append("g").attr("id", "clock-hands");
hands
.selectAll("line")
.data(handData)
.enter()
.append("line")
.attr("class", d => d.type + "-hand")
.attr("x1", 0)
.attr("y1", d => d.balance || 0)
.attr("x2", 0)
.attr("y2", d => d.length)
.attr("transform", d => `rotate(${d.scale(d.value)})`);
face
.append("g")
.attr("id", "face-overlay")
.append("circle")
.attr("class", "hands-cover")
.attr("x", 0)
.attr("y", 0)
.attr("r", clockRadius / 20);
}
function moveHands() {
const sel = d3
.select("#clock-hands-final")
.selectAll("line")
.data(handData)
.transition();
if (fancySecondsOFF) sel.ease(d3.easeElastic.period(0.5));
sel.attr("transform", d => `rotate(${d.scale(d.value)})`);
}
function updateData() {
handData[0].value = !fancySecondsOFF ? Math.floor(selectedExact * 10) : decTime[0];
handData[1].value = !fancySecondsOFF ? Math.floor(selectedExact * 10 % 1 * 100) : decTime.slice(2, 4);
handData[2].value = !fancySecondsOFF ? selectedExact * 10 % 1 * 100 % 1 * 100 : decTime.slice(4, 6);
}
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, w, h])
.style("max-width", `${width / 2.1}px`)
.attr("class", "analogclock")
.attr("id", "topclock");
svg
.append("text")
.text(`+${decTime}-${selectedZone}`)
.attr("x", clockRadius + margin)
.attr("y", clockRadius * 2 + margin * 2.1)
.attr("text-anchor", "middle")
.attr("font-size", 32)
.attr("font-family", "monospace");
drawClock();
// Animation
const interval = setInterval(
() => {
updateData();
moveHands();
},
!fancySecondsOFF ? 10 : 864
);
invalidation.then(() => clearInterval(interval));
return svg.node();
}// https://observablehq.com/@d3/simple-clock
// https://observablehq.com/@drio/lets-build-an-analog-clock
negclock = {
const clockRadius = 200,
margin = 50,
w = (clockRadius + margin) * 2,
h = (clockRadius + margin) * 2,
hourHandLength = (2 * clockRadius) / 3,
minuteHandLength = clockRadius,
secondHandLength = clockRadius - 12,
secondHandBalance = 30,
secondTickStart = clockRadius,
secondTickLength = -10,
hourTickStart = clockRadius,
hourTickLength = -18,
secondLabelRadius = clockRadius + 16,
secondLabelYOffset = 5,
hourLabelRadius = clockRadius - 40,
hourLabelYOffset = 7,
radians = Math.PI / 180;
const ten = d3
.scaleLinear()
.range([0, 360])
.domain([0, 10]);
const sto = d3
.scaleLinear()
.range([0, 360])
.domain([0, 100]);
const handData = [
{
type: "hour",
value: 0,
length: -hourHandLength,
scale: ten
},
{
type: "minute",
value: 0,
length: -minuteHandLength,
scale: sto
},
{
type: "second",
value: 0,
length: -secondHandLength,
scale: sto,
balance: secondHandBalance
}
];
function drawClock() {
// create all the clock elements
updateData(); //draw them in the correct starting position
const face = svg
.append("g")
.attr("id", "clock-face")
.attr("transform", `translate(${[w / 2, h / 2]})`);
// add marks for seconds
face
.selectAll(".second-tick")
.data(d3.range(0, 100))
.enter()
.append("line")
.attr("class", "second-tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", secondTickStart)
.attr("y2", secondTickStart + secondTickLength)
.attr("transform", d => `rotate(${sto(d)})`);
// and labels...
face
.selectAll(".second-label")
.data(d3.range(0, 100, 5))
.enter()
.append("text")
.attr("class", "second-label")
.attr("text-anchor", "middle")
.attr("x", d => secondLabelRadius * Math.sin(sto(d) * radians))
.attr(
"y",
d =>
-secondLabelRadius * Math.cos(sto(d) * radians) + secondLabelYOffset
)
.text(d => d);
// ... and hours
face
.selectAll(".hour-tick")
.data(d3.range(0, 10, 1))
.enter()
.append("line")
.attr("class", "hour-tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", hourTickStart)
.attr("y2", hourTickStart + hourTickLength)
.attr("transform", d => `rotate(${ten(d)})`);
face
.selectAll(".hour-label")
.data(d3.range(0, 10, 1))
.enter()
.append("text")
.attr("class", "hour-label")
.attr("text-anchor", "middle")
.attr("x", d => hourLabelRadius * Math.sin(ten(d) * radians))
.attr(
"y",
d => -hourLabelRadius * Math.cos(ten(d) * radians) + hourLabelYOffset
)
.text(d => d);
const hands = face.append("g").attr("id", "clock-hands");
hands
.selectAll("line")
.data(handData)
.enter()
.append("line")
.attr("class", d => d.type + "-hand")
.attr("x1", 0)
.attr("y1", d => d.balance || 0)
.attr("x2", 0)
.attr("y2", d => d.length)
.attr("transform", d => `rotate(${d.scale(d.value)})`);
face
.append("g")
.attr("id", "face-overlay")
.append("circle")
.attr("class", "hands-cover")
.attr("x", 0)
.attr("y", 0)
.attr("r", clockRadius / 20);
}
function moveHands() {
const sel = d3
.select("#clock-hands-final")
.selectAll("line")
.data(handData)
.transition();
if (fancySecondsOFF) sel.ease(d3.easeElastic.period(0.5));
sel.attr("transform", d => `rotate(${d.scale(d.value)})`);
}
function updateData() {
handData[0].value = !fancySecondsOFF ? Math.floor(selectedExactN * 10) : decTimeN[0];
handData[1].value = !fancySecondsOFF ? Math.floor(selectedExactN * 10 % 1 * 100) : decTimeN.slice(2, 4);
handData[2].value = !fancySecondsOFF ? selectedExactN * 10 % 1 * 100 % 1 * 100 : decTimeN.slice(4, 6);
}
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, w, h])
.style("max-width", `${width / 2.1}px`)
.attr("class", "analogclock")
.attr("id", "midclock");
svg
.append("text")
.text(`-${decTimeN}-${selectedZone}`)
.attr("x", clockRadius + margin)
.attr("y", clockRadius * 2 + margin * 2.1)
.attr("text-anchor", "middle")
.attr("font-size", 32)
.attr("font-family", "monospace");
drawClock();
// Animation
const interval = setInterval(
() => {
updateData();
moveHands();
},
!fancySecondsOFF ? 10 : 864
);
invalidation.then(() => clearInterval(interval));
return svg.node();
}// https://observablehq.com/@d3/simple-clock
// https://observablehq.com/@drio/lets-build-an-analog-clock
dotclock = {
const clockRadius = 200,
margin = 50,
w = (clockRadius + margin) * 2,
h = (clockRadius + margin) * 2,
hourHandLength = (2 * clockRadius) / 3,
minuteHandLength = clockRadius,
secondHandLength = clockRadius - 12,
secondHandBalance = 30,
secondTickStart = clockRadius,
secondTickLength = -10,
hourTickStart = clockRadius,
hourTickLength = -18,
secondLabelRadius = clockRadius + 16,
secondLabelYOffset = 5,
hourLabelRadius = clockRadius - 40,
hourLabelYOffset = 7,
radians = Math.PI / 180;
const ten = d3
.scaleLinear()
.range([0, 360])
.domain([0, 10]);
const sto = d3
.scaleLinear()
.range([0, 360])
.domain([0, 100]);
const handData = [
{
type: "hour",
value: 0,
length: -hourHandLength,
scale: ten
},
{
type: "minute",
value: 0,
length: -minuteHandLength,
scale: sto
},
{
type: "second",
value: 0,
length: -secondHandLength,
scale: sto,
balance: secondHandBalance
}
];
function drawClock() {
// create all the clock elements
updateData(); //draw them in the correct starting position
const face = svg
.append("g")
.attr("id", "clock-face")
.attr("transform", `translate(${[w / 2, h / 2]})`);
// add marks for seconds
face
.selectAll(".second-tick")
.data(d3.range(0, 100))
.enter()
.append("line")
.attr("class", "second-tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", secondTickStart)
.attr("y2", secondTickStart + secondTickLength)
.attr("transform", d => `rotate(${sto(d)})`);
// and labels...
face
.selectAll(".second-label")
.data(d3.range(0, 100, 5))
.enter()
.append("text")
.attr("class", "second-label")
.attr("text-anchor", "middle")
.attr("x", d => secondLabelRadius * Math.sin(sto(d) * radians))
.attr(
"y",
d =>
-secondLabelRadius * Math.cos(sto(d) * radians) + secondLabelYOffset
)
.text(d => d);
// ... and hours
face
.selectAll(".hour-tick")
.data(d3.range(0, 10, 1))
.enter()
.append("line")
.attr("class", "hour-tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", hourTickStart)
.attr("y2", hourTickStart + hourTickLength)
.attr("transform", d => `rotate(${ten(d)})`);
face
.selectAll(".hour-label")
.data(d3.range(0, 10, 1))
.enter()
.append("text")
.attr("class", "hour-label")
.attr("text-anchor", "middle")
.attr("x", d => hourLabelRadius * Math.sin(ten(d) * radians))
.attr(
"y",
d => -hourLabelRadius * Math.cos(ten(d) * radians) + hourLabelYOffset
)
.text(d => d);
const hands = face.append("g").attr("id", "clock-hands");
hands
.selectAll("line")
.data(handData)
.enter()
.append("line")
.attr("class", d => d.type + "-hand")
.attr("x1", 0)
.attr("y1", d => d.balance || 0)
.attr("x2", 0)
.attr("y2", d => d.length)
.attr("transform", d => `rotate(${d.scale(d.value)})`);
face
.append("g")
.attr("id", "face-overlay")
.append("circle")
.attr("class", "hands-cover")
.attr("x", 0)
.attr("y", 0)
.attr("r", clockRadius / 20);
}
function moveHands() {
const sel = d3
.select("#clock-hands-final")
.selectAll("line")
.data(handData)
.transition();
if (fancySecondsOFF) sel.ease(d3.easeElastic.period(0.5));
sel.attr("transform", d => `rotate(${d.scale(d.value)})`);
}
function updateData() {
handData[0].value = selTimeCmd[0];
handData[1].value = selTimeCmd.slice(1, 3);
handData[2].value = selTimeCmd.slice(3, 5);
}
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, w, h])
.style("max-width", `${width / 2.1}px`)
.attr("class", "analogclock")
.attr("id", "topclock");
svg
.append("text")
.text(`+${selTimeTen.toFixed(4)}`)
.attr("x", clockRadius + margin)
.attr("y", clockRadius * 2 + margin * 2.1)
.attr("text-anchor", "middle")
.attr("font-size", 32)
.attr("font-family", "monospace");
drawClock();
// Animation
const interval = setInterval(
() => {
updateData();
moveHands();
},
!fancySecondsOFF ? 10 : 864
);
invalidation.then(() => clearInterval(interval));
return svg.node();
}sunclock = {
const radius = 200;
const width = radius * 2;
const height = radius * 2;
//let _minutes = 0;
const svg = d3
.create("svg")
.attr("id", "btmclock")
.attr("viewBox", [0, 0, width, height])
.style("margin-top", "1px")
.style("max-width", "200px");
const face = svg
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
for (const angle of angles) {
const arc = d3
.arc()
.innerRadius(0)
.outerRadius(radius)
.startAngle(angle.start)
.endAngle(angle.end);
face.append("path").attr("d", arc).attr("fill", angle.color);
}
const sun = face
.append("circle")
.attr("r", 15)
.attr("cx", 125 * -sin(astDegMapHalf))
.attr("cy", 125 * cos(astDegMapHalf))
.style("fill", "white");
const ringContainer = svg
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
const ring = ringContainer
.append("circle")
.attr("r", 125)
.style("opacity", 0.2)
.style("fill", "transparent")
.style("stroke", "white")
.style("stroke-width", 3);
const events = [
data.astronomicalSunrise,
data.astronomicalSunset,
data.nauticalSunrise,
data.nauticalSunset,
data.civilSunrise,
data.civilSunset,
data.sunrise,
data.sunset,
data.solarNoon
];
for (const e of events) {
ringContainer
.append("circle")
.attr("r", 5)
.attr("cx", 125 * Math.sin(e))
.attr("cy", 125 * -Math.cos(e))
.style("opacity", 0.4)
.style("fill", "white");
}
const digitalClock = svg
.append("g")
.attr(
"transform",
`translate(${width / 2 + 45 * sin(astDegMapHalf)}, ${
height / 2 + 45 * -cos(astDegMapHalf)
})`
);
const digitalClockBase = digitalClock
.append("circle")
.attr("r", 100)
.style("opacity", 0.21)
.style("fill", "white");
// const digitalClockTicks = digitalClock
// .selectAll(".digital-clock-tick")
// .data(d3.range(0, 60))
// .enter()
// .append("line")
// .attr("class", "digital-clock-tick")
// .attr("x1", 0)
// .attr("x2", 0)
// .attr("y1", 100 - 5)
// .attr("y2", 100 - 10)
// .attr("stroke", (d) => (d > seconds ? "black" : "white"))
// .attr("stroke-width", "3")
// .attr("stroke-linecap", "round")
// .style("mix-blend-mode", "luminance")
// .style("opacity", 0.6)
// .attr("transform", (d) => `rotate(${180 + d * 6})`);
const clockText = digitalClock
.append("text")
.attr("y", 25)
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("font-family", "Helvetica, Arial, sans-serif")
.style("opacity", 1)
.attr("font-weight", 600)
.attr("font-size", "75px")
.text(astDecMapPlot.toFixed(2));
const ticks = face
.selectAll(".tick")
.data(d3.range(0, 20))
.enter()
.append("line")
.attr("class", "tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", radius - 5)
.attr("y2", radius - 20)
.attr("stroke", "white")
.attr("stroke-width", "3")
.attr("stroke-linecap", "round")
.style("mix-blend-mode", "screen")
.style("opacity", 1)
.attr("transform", (d) => `rotate(${d * 18})`);
const minuteTicks = face
.selectAll(".tick")
.data(d3.range(0, 200))
.enter()
.append("line")
.attr("class", "tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", radius - 5)
.attr("y2", radius - 15)
.attr("stroke", "white")
.attr("stroke-width", "2")
.attr("stroke-linecap", "round")
.style("mix-blend-mode", "screen")
.style("opacity", 0.6)
.attr("transform", (d) => `rotate(${d * 2})`);
const hourLabels = face
.selectAll(".hour-label")
.data(d3.range(0, 10))
.enter()
.append("text")
.attr("class", "hour-label")
.attr("x", (d) => 160 * -Math.sin(toRadians((d / 10) * 360)))
.attr("y", (d) => 160 * Math.cos(toRadians((d / 10) * 360)) + 12)
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("font-family", "Helvetica, Arial, sans-serif")
.attr("font-weight", 600)
.attr("font-size", "38px")
.style("opacity", 0.7)
.text((d) => (String((d + 5) % 10)));
return svg.node();
}
minutesAndSeconds = {
while (true) {
await Promises.delay(1000);
const date = new Date();
yield {
minutes: date.getHours() * 60 + date.getMinutes(),
seconds: date.getSeconds()
};
}
}
function minutesTo12HourTime(minutes) {
const decidays = minutes / 144 % 10
return decidays.toFixed(2);
}
viewof date = Inputs.date({ label: "Date", value: Date.now() })
JD = getJulianDate(date)
T = (JD - 2451545.0) / 36525
sun_geometric_mean_longitude = 280.46646 + 36000.76983 * T + 0.0003032 * T ** 2
sun_mean_anomaly = 357.52911 + 35999.05029 * T - 0.0001537 * T ** 2
earth_eccentricity_orbit = 0.016708634 - 0.000042037 * T - 0.0000001267 * T ** 2
sun_equation_of_the_center = (1.914602 - 0.004817 * T - 0.000014 * T ** 2) * sin(sun_mean_anomaly) + (0.019993 - 0.000101 * T) * sin(2 * sun_mean_anomaly) + 0.000289 * sin(3 * sun_mean_anomaly)
sun_true_longitude = sun_geometric_mean_longitude + sun_equation_of_the_center
sun_true_anomaly = sun_mean_anomaly + sun_equation_of_the_center
omega = 125.04 - 1934.136 * T
sun_aparrent_longitude = sun_true_longitude - 0.00569 - 0.00478 * sin(omega)
sun_ecliptic_obliquity = 23 + (26 + (21.448 - 46.815 * T - 0.00059 * T ** 2 + 0.001813 * T ** 3) / 60) / 60
sun_ecliptic_obliquity_corrected = sun_ecliptic_obliquity + 0.00256 * cos(omega)
sun_right_ascension = Math.atan2(
cos(sun_ecliptic_obliquity_corrected) * sin(sun_aparrent_longitude),
cos(sun_aparrent_longitude)
)
sun_declination = Math.asin(
sin(sun_ecliptic_obliquity_corrected) * sin(sun_aparrent_longitude)
)
y = Math.tan(toRadians(sun_ecliptic_obliquity_corrected) / 2) ** 2
equation_of_time = toDegrees(
y * sin(2 * (sun_geometric_mean_longitude)) -
2 * earth_eccentricity_orbit * sin((sun_mean_anomaly)) +
4 * earth_eccentricity_orbit * y * sin((sun_mean_anomaly)) * cos(2 * (sun_geometric_mean_longitude)) -
(1 / 2) * y ** 2 * sin(4 * (sun_geometric_mean_longitude)) -
(5 / 4) * earth_eccentricity_orbit ** 2 * sin(2 * (sun_mean_anomaly))
) * 4
hour_angle = shaDegrMapPlot
sun_transit = solarNoonMinu
sun_rise = sun_transit - hour_angle * 4
sun_set = sun_transit + hour_angle * 4
sun_duration = sun_set - sun_rise
sun_civil_rise = sun_transit - (hour_angle + 6) * 4
sun_nautical_rise = sun_transit - (hour_angle + 12) * 4
sun_astronomical_rise = sun_transit - (hour_angle + 18) * 4
sun_civil_set = sun_transit + (hour_angle + 6) * 4
sun_nautical_set = sun_transit + (hour_angle + 12) * 4
sun_astronomical_set = sun_transit + (hour_angle + 18) * 4
data = ({
astronomicalSunrise:
toRadians((sun_astronomical_rise / 1440) * 360) + Math.PI,
nauticalSunrise: toRadians((sun_nautical_rise / 1440) * 360) + Math.PI,
civilSunrise: toRadians((sun_civil_rise / 1440) * 360) + Math.PI,
sunrise: toRadians((sun_rise / 1440) * 360) + Math.PI,
sunset: toRadians((sun_set / 1440) * 360) + Math.PI,
civilSunset: toRadians((sun_civil_set / 1440) * 360) + Math.PI,
nauticalSunset: toRadians((sun_nautical_set / 1440) * 360) + Math.PI,
astronomicalSunset: toRadians((sun_astronomical_set / 1440) * 360) + Math.PI,
solarNoon: toRadians((sun_transit / 1440) * 360) + Math.PI,
dayLength: sun_duration
})
angles = [
{
start: data.astronomicalSunrise,
end: data.nauticalSunrise,
color: "rgb(17,49,86)"
},
{
start: data.nauticalSunrise,
end: data.civilSunrise,
color: "rgb(23,61,112)"
},
{ start: data.civilSunrise, end: data.sunrise, color: "rgb(35,85,155)" },
{ start: data.sunrise, end: data.sunset, color: "rgb(70,155,245)" },
{ start: data.sunset, end: data.civilSunset, color: "rgb(35,85,155)" },
{
start: data.civilSunset,
end: data.nauticalSunset,
color: "rgb(23,61,112)"
},
{
start: data.nauticalSunset,
end: data.astronomicalSunset,
color: "rgb(17,49,86)"
},
{
start: data.astronomicalSunset,
end: data.astronomicalSunrise + Math.PI * 2,
color: "rgb(10,34,58)"
}
]
getJulianDate = (timestamp) => {
return new Date(timestamp) / 86400000 + 2440587.5;
}
sin = (x) => Math.sin(toRadians(x))
cos = (x) => Math.cos(toRadians(x))
solarNoonTurn = (9.55 + selectedZone / 10 - lonTurnMapPlot - eotTurnMapPlot + .5) % 1
solarNoonMinu = solarNoonTurn * 1440
majwid = Math.min(5879, width) // majorized width
// https://observablehq.com/@harrystevens/circles-angles-and-right-triangles
daradius = 100
daheight = 250
dacenter = [ daradius + 38, daheight / 2 ]
geometric = require("geometric@2")
daangle = dayArcInput * .36 / 2
daend = geometric.pointTranslate(dacenter, -daangle + 90, daradius)
daend1 = geometric.pointTranslate(dacenter, daangle + 90, daradius)html`
<style>
.colorNight {
background: #416f86;
color: white;
padding: 0px 2px 0px 4px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDay {
background: #ffe438;
color: black;
padding: 0px 3px 0px 4px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorBkg {
background: ${window.darkmode ? "black" : "white"};
color: ${window.darkmode ? "white" : "black"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0 {
background: hsl(0 100% 50%);
color: ${yiq(`hsl(0, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color1by8640 {
background: hsl(${h1by8640} 100% 50%);
color: ${yiq(`hsl(${h1by8640}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color1 {
background: hsl(300 100% 50%);
color: ${yiq(`hsl(300, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color2 {
background: hsl(280 100% 50%);
color: ${yiq(`hsl(280, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color3 {
background: hsl(240 100% 50%);
color: ${yiq(`hsl(240, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color4 {
background: hsl(200 100% 50%);
color: ${yiq(`hsl(200, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color5 {
background: hsl(180 100% 50%);
color: ${yiq(`hsl(180, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color583 {
background: hsl(129.88235294117646 100% 50%);
color: ${yiq(`hsl(129.88235294117646, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color6 {
background: hsl(120 100% 50%);
color: ${yiq(`hsl(120, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color7 {
background: hsl(80 100% 50%);
color: ${yiq(`hsl(80, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color8 {
background: hsl(60 100% 50%);
color: ${yiq(`hsl(60, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color9 {
background: hsl(40 100% 50%);
color: ${yiq(`hsl(40, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color375 {
background: hsl(${hues[0.375]} 100% 50%);
color: ${yiq(`hsl(${hues[0.375]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0083 {
background: hsl(${hues[0.0083]} 100% 50%);
color: ${yiq(`hsl(${hues[0.0083]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0166 {
background: hsl(${hues[0.0166]} 100% 50%);
color: ${yiq(`hsl(${hues[0.0166]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color55by24 {
background: hsl(${hues[.0229]} 100% 50%);
color: ${yiq(`hsl(${hues[.0229]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color125 {
background: hsl(${hues[.125]} 100% 50%);
color: ${yiq(`hsl(${hues[.125]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color025 {
background: hsl(${hues[0.025]} 100% 50%);
color: ${yiq(`hsl(${hues[0.025]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color287 {
background: hsl(${hues[0.287]} 100% 50%);
color: ${yiq(`hsl(${hues[0.287]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color333 {
background: hsl(${hues[0.333]} 100% 50%);
color: ${yiq(`hsl(${hues[0.333]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0333 {
background: hsl(${hues[0.0333]} 100% 50%);
color: ${yiq(`hsl(${hues[0.0333]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0416 {
background: hsl(${hues[0.0416]} 100% 50%);
color: ${yiq(`hsl(${hues[0.0416]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color429 {
background: hsl(${hues[0.429]} 100% 50%);
color: ${yiq(`hsl(${hues[0.429]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color05 {
background: hsl(${hues[0.05]} 100% 50%);
color: ${yiq(`hsl(${hues[0.05]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color533 {
background: hsl(${hues[0.533]} 100% 50%);
color: ${yiq(`hsl(${hues[0.533]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color969 {
background: hsl(${hues[0.969]} 100% 50%);
color: ${yiq(`hsl(${hues[0.969]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color999 {
background: hsl(${hues[0.999]} 100% 50%);
color: ${yiq(`hsl(${hues[0.999]}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color1by320 {
background: hsl(${h1by320} 100% 50%);
color: ${yiq(`hsl(${h1by320}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD121 {
background: hsl(${hD121} 100% 50%);
color: ${yiq(`hsl(${hD121}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD268 {
background: hsl(${hD268} 100% 50%);
color: ${yiq(`hsl(${hD268}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD305 {
background: hsl(${hD305} 100% 50%);
color: ${yiq(`hsl(${hD305}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD306 {
background: hsl(${hD306} 100% 50%);
color: ${yiq(`hsl(${hD306}, 100%, 50%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.posneg {
position: relative;
top: ${width < 770 ? 274 : width < 780 ? 264 : width < 790 ? 254 : width < 800 ? 244 : width < 810 ? 234 : width < 816 ? 228 : width < 819 ? 218 : width < 823 ? 188 : width < 825 ? 168 : width < 830 ? 162 : width < 840 ? 154 : width < 848 ? 134 : width < 850 ? 124 : width < 851 ? 114 : width < 852 ? 112 : width < 854 ? 110 : width < 856 ? 108 : width < 858 ? 106 : width < 860 ? 104 : width < 862 ? 102 : width < 864 ? 100 : width < 865 ? 84 : width < 866 ? 72 : width < 872 ? 70 : width < 874 ? 68 : width < 876 ? 66 : width < 878 ? 64 : width < 888 ? 62 : width < 890 ? 60 : width < 892 ? 58 : width < 894 ? 56 : width < 896 ? 54 : width < 898 ? 52 : width < 900 ? 46 : width < 907 ? 52 : width < 913 ? 56 : width < 918 ? 58 : width < 923 ? 60 : width < 933 ? 68 : width < 943 ? 78 : width < 973 ? 88 : width < 978 ? 68 : width < 985 ? 44 : width < 988 ? 42 : width < 992 ? 40 : width < 993 ? 148 : width < 1005 ? 154 : width < 1020 ? 148 : width < 1045 ? 142 : width < 1070 ? 124 : width < 1075 ? 112 : width < 1080 ? 108 : width < 1086 ? 96 : width < 1088 ? 87 : width < 1090 ? 78 : width < 1093 ? 64 : width < 1095 ? 38 : width < 1100 ? 36 : width < 1110 ? 22 : width < 1120 ? 18 : width < 1130 ? 12 : width < 1140 ? 14 : width < 1150 ? 16 : width < 1160 ? 18 : width < 1170 ? 20 : width < 1180 ? 24 : width < 1185 ? 26 : width < 1190 ? 28 : width < 1195 ? 30 : width < 1200 ? 32 : width < 1221 ? 28 : width < 1224 ? 10 : width < 1226 ? -26 : width < 1231 ? -24 : width < 1234 ? -31 : width < 1237 ? -37 : width < 1239 ? -40 : width < 1241 ? -42 : width < 1243 ? -40 : width < 1245 ? -38 : width < 1246 ? -46 : width < 1247 ? -48 : width < 1250 ? -50 : width < 1300 ? -52 : width < 5600 ? -54 : -width / 104}px;
}
</style>
`Reuse
Citation
@online{laptev2024,
author = {Laptev, Martin},
title = {Dec {Time}},
date = {2024},
urldate = {2024},
url = {https://maptv.github.io/dec/time},
langid = {en}
}