%%{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+154
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 analog clocks🕓, bar📊charts, solar☀️terminator map, Earth🌍orbit diagram, and daylight area chart below.
Fractional day time
Dec times are measured in fractional days. The shortest, longest, and thinnest clock🕓hands and 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📊.
Ticking analog clocks
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();
}
Longitude latitude map
Daylight area chart
app = {
const svg = d3.select(DOM.svg(width, height * (width < 300 ? .97 : width < 350 ? .96 : width < 400 ? .95 : width < 450 ? .94 : width < 500 ? .93 : width < 550 ? .92 : width < 600 ? .9 : width < 650 ? .86 : width < 700 ? .82 : .78)));
svg.style("user-select", "none").style("-webkit-user-select", "none").attr("id", "daylightapp");
const margin = {top: 20, left: 16, right: 16, bottom: 0, inner: 32};
const contentWidth = width - margin.left - margin.right - margin.inner;
const columnWidth = contentWidth / 2;
let selection = {
date: date2022,
hour: date2022.getHours()
}
const renderPlot = () => {
svg.selectAll("#plot *").remove();
svg.select("#plot").call(daylightPlot, {
vizwidth: columnWidth / (width < 300 ? 1 : width < 400 ? 1.05 : width < 450 ? 1.1 : width < 500 ? 1.15 : width < 550 ? 1.2 : width < 600 ? 1.25 : width < 650 ? 1.3 : width < 700 ? 1.4 : 1.48),
height: height * (width < 400 ? 1.62 : width < 500 ? 1.6 : width < 700 ? 1.58 : 1.56),
year: 2022,
latitude: location[1],
defaultDate: selection.date,
defaultHour: selection.hour
})
}
const renderSolarSystem = () => {
svg.selectAll("#solar-system *").remove();
svg.selectAll("#solar-system").call(solarSystem,
columnWidth * 2.02,
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;
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 + 0}, ${height / 5 + (width < 600 ? 12 : 5)})`);
svg.append("g")
.attr("id", "solar-system")
.attr("transform", `translate(${margin.left + 12}, ${margin.top + width / 22 - 6 - 5 * (width < 400)})`);
// 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();
}
// https://observablehq.com/@d3/simple-clock
// https://observablehq.com/@drio/lets-build-an-analog-clock
clock = {
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();
}
clock1 = {
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", "btmclock");
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();
}
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.
Yearly day aggregate (yda)
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 “yearly day aggregate” (yda): .
\[\text{yda} = \text{doy} + \text{tod}\]
As their names suggest, doys and ydas 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)
We can turn an yda into a tod by keeping the remainder after dividing by one to isolate the decimal part of the quotient: mod 1 = . We can use this same approach to obtain a tod from an “epochal day aggregate” (eda): mod 1 = . The current eda tells us how many days have passed since the Dec epoch.
\[\text{tod} = \text{yda} \text{ mod } 1 = \text{eda} \text{ mod } 1\]
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.7602237
(as.numeric(as.POSIXct(hms)) / 86400) %% 1
[1] 0.7602237
The equations below turn 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.
\[\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\]
dsa <- (as.numeric(as.POSIXct(Sys.time())) / 86400) %% 1 * 86400
hsa <- dsa %% 3600
sapply(c(dsa %/% 3600, hsa %/% 60, hsa %% 60), as.integer)
[1] 0 17 5
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 1.0416 centimillidays (cmds) of Universal Time (UT).
Time zone offset
Instead of leap seconds, Dec matches UT using a “time zone offset” (tzo). With the leap second insertion dates provided by the International Earth Rotation and Reference Systems Service, we can approximate the tzo we need to add to the Zone 0 tod to obtain UT on the year+day
Dec date selected by the range🎚️inputs below: ÷ 8640 = .
Apparent solar time
Both UT and apparent solar time (ast) vary over time. Unlike UT, ast also varies by longitude and is cyclical on a yearly basis. To approximate ast for a given deciparallel longitude (\(\text d\lambda\)) and doy, we sum the Zone 0 tod, \(\text d\lambda\), and the result obtained from plugging the doy into the equation of time (eot): + + = .
The National Oceanic and Atmospheric Administration (NOAA) provides yearly and daily eot values. To create a Dec version of the eot, we can sort the yearly NOAA eot values by their doy and then fit a curve to the sorted values so that we can use the fitted model coefficients (\(\beta_0\) to \(\beta_4\)) as the eot constants in the NOAA General Solar Position Calculations:
\[\tau = 2 \times \pi\]
\[\text{toy} = \text{doy} \div \text{n}\]
\[\gamma = \tau \times \text{toy}\]
\[\begin{split} \text{eot}(\gamma) & = \beta_0 \\ & + \beta_1 \times \cos(\gamma) \\ & + \beta_2 \times \sin(\gamma) \\ & + \beta_3 \times \cos(2\gamma) \\ & + \beta_4 \times \sin(2\gamma) \end{split}\]
\[\text{mst} = \text{tod} + \text{d}\lambda\]
\[\text{ast} = (\text{mst} + \text{eot}(\gamma) + 10) \text{ mod } 10\]
Equation of time
In the equations above, \(\tau\) is two times \(\pi\), n is the number of days in a year, the “time of year” (toy) is the doy divided by n, \(\gamma\) is the toy multiplied by \(\tau\), “mean solar time” (mst) is the Zone 0 tod plus \(\text d\lambda\). We can refer to \(\text d\lambda\) as the mst tzo and the sum of \(\text d\lambda\) and eot(\(\gamma\)) as the ast tzo. Using the coefficients shown below, we can plot eot(\(\gamma\)) against doy:
{0: 1.4950424467725955e-05,
1: -0.04189873301524992,
2: -0.02923703633324393,
3: -0.0456943801628261,
4: 0.05175601752530743}
// 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: "time zone offset"},
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;
}
Solar declination angle
We can subtract the ast tzo from five to get the Zone 0 tod of solar noon at the given longitude. Before we can find the sunrise and sunset tod, we first need to calculate the solar declination angle (sda). For simplicity, we can use the eot for sda, even though the NOAA General Solar Position Calculations propose the more complex model shown below.
\[\text{noon} = (5 - \text{d}\lambda - \text{eot}(\gamma) + 10) \text{ mod } 10\]
\[\begin{split} \text{sda}(\gamma) & = \beta_0 \\ & + \beta_1 \times \cos(\gamma) \\ & + \beta_2 \times \sin(\gamma) \\ & + \beta_3 \times \cos(2\gamma) \\ & + \beta_4 \times \sin(2\gamma) \\ & + \beta_5 \times \cos(3\gamma) \\ & + \beta_6 \times \sin(3\gamma) \\ \end{split}\]
Solar hour angle
If the “solar zenith angle” (sza) in the equation below is 0.2523 turns, the equation will yield a sunrise “solar hour angle” (sha) that we can use to get a sunrise or sunset tod. Dec measures angles in turns. The trigonometric functions of most — if not all – programming languages require radians. To convert turns into radians, we multiply by \(\tau\).
\[\text{radian} = \text{turn} \times \tau = \text{degree} \times \tau \div 360\]
\[\text{turn} = \text{radian} \div \tau = \text{degree} \div 360\]
\[\text{degree} = \text{radian} \div \tau \div 360 = \text{turn} \times 360\]
\[\text{sha} = \arccos\left( \frac{\cos(\text{sza}\times\tau)}{\cos(\phi\times\tau) \cdot \cos(\text{sda}\times\tau)} - \tan(\text{$\phi\times\tau$}) \cdot \tan(\text{sda}\times\tau) \right)\]
In the equation above, latitude is measured in turns called meridians (\(\phi\)). The difference between a solar noon and sunrise sha is a sunrise tod. Likewise, the sum of a solar noon and sunrise sha is a sunset tod. To get the daylight duration in decidays, we can either subtract the sunset tod from the sunrise tod or multiply the sunrise sha by twenty.
\[\text{sunrise} = \text{noon} - \text{sha}\]
\[\text{sunset} = \text{noon} + \text{sha}\]
\[\text{duration} = \text{sunset} - \text{sunrise} = \text{sha} \times 20\]
In general, computer programmers and mathematicians seem to prefer radians over degrees. are better because the Dec base units of time, longitude, and latitude can be multiplied by \(\tau\) to can convert days, parallels, and meridians into radians simply by multiplying to . Apart from such as solar noon, In Dec, any tod can be expanded to show a subtrahend and a difference. specific point in the day called a subtrahend. The difference between the original tod and a . A tod without the diff is called a minuend and Positive diffs show how much time has passed and negative diffs show how much time is left.
\[\text{minuend} = \text{subtrahend} + \text{diff}\]
\[\text{minuend} = \text{subtrahend} + \text{diff}\]
The Zone 0 tod relative to the solar noon that corresponds to the doy and longitude selected by the range🎚️inputs above is .
In Zone 0, this was our solar noon, we could could all agree to center our daily schedules on the Zone 0 tod of our local solar noon, there would be no need for time zones. could start work two decidays before solar noon and finish two decidays after solar noon.
depends on longitude. At any given moment, there is only one UT and 105 five-digit mean solar times. The tzo we use to get the mean solar time of a location is equal to its deciparallel longitude. We can use the equation of time to turn the mean solar time of into apparent solar time.
sum of a tzo and the Zone 0 tod. We can use the equation of time to turn the mean solar time of a location Similarly, the deciparallel longitude of the red⭕️circle on the map above is the tzo that we sum with the Zone 0 tod to get mean solar time.
both cases, we are adding a tzo to the We can turn mean solar time A Dec time consists of a tod and a tzo. The current Dec time in Zone 0 is -0. If we add 27 seconds to the Zone 0 time, we will get the time in Zone 0.003125: -0.003125. We can evaluate any Dec time like a math expression to obtain a Zone 0 tod: - 0.003125 = .
Without leap seconds, the duration of a day is constant, each hms triplet occurs only once per day, hms triplets can be derived directly from UNIX time or a tod independently of the date as shown above, and the som component stays within its bounds: 0 ≤ som < 60. UTC is based on atomic clocks and UT is determined by the rotation of the Earth. rom the perspective of Dec,
that represents the difference between UT and the Zone 0 tod.
To display hms triplets, Dec uses math expressions that evaluate to a deciday tod and leave no doubt about the measurement unit of each component: hod/2.4+moh/144+som/8640. Like the Julia, Matlab, and R programming languages, Dec uses colons for numeric sequences such as 12:15, which means 12, 13, 14, and 15 instead of 12.25 hours.
Dec uses centimillidays (cmds) in lieu of seconds. Centimillidays Instead of inserting a submultiple of a day into tods, would be to follow the switch from base 10 to base 11 on a specific date and then switch back. The last tod on that date would be 9.999Abbbbbbbbbbbbb able to take leap seconds into account without changing the duration of a day, repeating a tod within the same day, handles leap seconds by converting them to decidays and adding them to a “time zone offset” (tzo). This way, Dec can match any time with any number of leap seconds the duration of a day never changes, hms triplets never repeat within the same day, Dec can match any possible All of the we can be certain that leap seconds do not exist, meaning that In Dec, days always have the same duration, we can obtain a tod without knowing the date, and tods never go outside their bounds or repeat within the same day. , of decimal inserts leap days to increase the duration of leap year from 365 to 366 days, but leap seconds cannot exist. Leap seconds /
Negative leap seconds are evidence If an hms triplet repeats, we will know that a had just been inserted. If an hms triplet exceeds its bounds, we will know that a positive leap second had just been inserted. In general, we cannot know how many leap seconds have been inserted thus far simply by looking at an hms triplet.
of sixty seconds in a minute. In Dec, every tod leap seconds are literally impossible, because days always have the same duration and tods are decimal numbers.
In Dec, days always have the same duration and tod calculations are independent of the date. Currently, 27 leap seconds To know how many leap seconds have been inserted by a given date, we have to consult a table. The duration of the days into which its are inserted. Instead of changing the duration of certain days, Dec handles leap seconds by converting them to decidays and adding them to a “time zone offset” (tzo).
The languages in the tabset panels in this article, Julia, Python, Observable JavaScript, and R, do not differ in this regard which Dec refers to as the “Quarto principal languages” (Qpls).
With the code above we can convert between a Zone 0 tod and a UTC hms triplet. According to Dec and all four of the programming languages used above, UTC does not have any leap seconds.
Dec always measures tzos in decidays. The tzos of the ten Dec time zones are all single-digit positive integers. The difference between these ten tzos, 0 to 9, we can match the time in any UTC time zone to within 0.5 decidays.
If more precise matches are required, Dec can use tzos that have more than one digit. The level of precision required depends on the situtation. Let us say that we want to schedule a meeting with someone If we want to replicate a shift form make the time in UTC time zones that have a tzo of -5, 0, or 5, a . At some point the increased precision of additional digits will no If we lived in Zone 0 and wanted to schedule a meeting with someone on a particular date, we might two digits, the time difference will not exceed 5 millidays and will be able to shift tzos by one centiday, which is less a quarter hour. With four digits, the difference between Dec and UTC tzos would be either zero or 288 milliseconds. We can choose to have more than four digits, but the difference is not likely to be noticeable. Therefore, four digits is a sensible If we wanted to match The UTC tzo used by the highest number of people in the world is 10/3.
There are three types of purpose of time zones is to keep To match the time in a UTC time zone exactly, Dec will often need to use a decimal tzo.
UTC, UNIX time, and Zone 0 all have a tzo of 0. tod and because both have a tzo of . If we add 27 leap seconds to UTC, Dec would describe the result as a Zone 0.003125 tod.
Each leap second shifts a tzo by 1/8640. By default, Dec rounds decimal tzos to the near deciday: ⌊tzo + 0.5⌋. When necessary, Dec is able to exactly match times that include leap seconds or a decimal tzo.
The result of adding leap seconds to a tzo depends on how many leap seconds we add. If we add a number of leap seconds that is a multiple of 8640, we get a
and deals with negative tzos keeping the remainder after adding ten and then dividing by ten: (tzo + 10) mod 10.
After combining a UTC tzo, Dec first combines it with to decidays, Dec rounds decimal tzos to the nearest deciday. and makes negative tzos that are positive integers. and therefore rounds
Unless To support leap seconds and the vast majority of UTC tzos, Dec tzos that are . If we add leap seconds to these tzos, they will no longer be integers
[
If a tzo is a repeating decimal, Dec will display it as a fraction.
0003125. new tzo will be 0.003125 and the new uzi will be . the Dec time will be -0003125. The 0003125 The uzi tells us that the go from 0 to [0003125] and the D
an hms triplet into a Dec time, we need to know its UTC tzo, and how many leap seconds it includes. a tod, from a programming language, it is can assume that leap seconds are not included. tzoThe Dec equivalent of UTC with is a Dec tod, tzo, uzi, If we add 27 leap seconds We can combine the tod in any Dec time zone by summing the Zone 0, 27 leap seconds to a Zone tod, we get add the result deciday and the tzo selected by the range🎚️input below is [0.003125] + = . If we add the resulting tzo to the Zone 0 tod, we get . last two digits of this tzo and uzi are not visible in a five-digit tod If we remove the decimal separator from this tzo, we get its uzi: .
Every tzo has a corresponding “time zone index” (tzi). In the case of the ten Dec time zones, the tzo and tzi are identical. The tzi that corresponds to the Dec measures tzos in decidays. If a tzo is a single-digit integer, it likely or fractions. The tzos of the ten Dec time zones are positive integers. contain leap seconds as fractions. Even if we disregard leap seconds, fractions in tods and tzos to are single-digit integers or fractions. Dec only uses ten Dec time zones each have a tzo that is a : 0 to 9 decidays. In contrast, UTC tzos range range from -5 to 5.83̅ decidays. A UTC time zone with a negative tzo will be between have a similar time To avoid date mismatches with , we can subtract ten decidays from any positive Dec tzo to make it negative: – 10 = .
far increase the Each leap second shifts the ozi by
Finding the Dec time zone with the closest tzo to a given UTC tzo entails converting an hms triplet to decidays, adding ten, keeping the remainder after dividing by ten, and then rounding to the nearest deciday. matching a tzo of -5 yields the same tod as exactly one day behind Zone [5 Dec avoids incompatible with the ten Dec time zones. Dec expresses incompatibles tzos as fractions. Officially, t Dec refers to UTC with 27 leap seconds as UTC+00:00:27 or UTC+1/320. Dec can relax its requirement that each tzo be a single-digit positive integer deciday to match any UTC tzo.
viewof hmsinput = Inputs.form([
Inputs.range([-24, 24], {label: "Hour", value: 0, step: 1}),
Inputs.range([0, 60], {label: "Minute", value: 0, step: 1}),
Inputs.range([0, 60], {label: "Second", value: 0, step: 1}),
])
tzos is
Dec Positive and negative offsets that differ by exactly ten decidays yield identical times, but result in dates that are precisely one day apart.
https://stackoverflow.com/a/23575642 Use fractions.js to turn non standard offsets into fractions.
To determine which Dec time zone you are in, Dec would need to know your longitude. In general, Dec allows for everyone to use whatever tzo they want, regardless of their geographic location. The UTC tzo provided by your web browser is -300 minutes ÷ 144 = -2.083333 dd. In the absence of any other information, Dec would round this tzo to the nearest deciday (dd), , and infer that you are in Zone 8. which Dec time zone you are in unless Rounding UTC tzos can result in a time difference of up to 0.5 dd. Adding 27 leap seconds increases the maximum time difference to 161/320 dd. With time zone rounding, the time difference between your Dec and UTC time zones is 11.52 ÷ 144 = 0.08 dd. With 27 leap seconds, this difference is /320.
The UTC offset provided by your web browser is ÷ 144 = dd. The Dec time zone that corresponds to this UTC offset is Zone . The time in corresponding Dec and UTC time zones can differ by up to 0.5 dd. The difference between your Dec and UTC time is ÷ 144 = dd.
To obtain the time in Zone 0, we can subtract the offset of any time zone from its time. Inversely, we can get the time in any time zone by adding its offset to the Zone 0 time. The dates and times in Zone 0 and UTC+00:00 match exactly. Zone 5 and UTC+12:00 also have matching dates and times, both are precisely one day ahead of UTC-12:00.
This stance is at odds with the International Earth Rotation and Reference Systems Service, which has inserted 27 leap seconds into UTC.
since [1972]. Dec does not automatically shift time away necessary, Dec handles any shift away from leap seconds in time zone offsets.
Leap seconds
To indicate the
In Dec, the components of an hms triplet are called the “hour of day” (hod), “minute of hour” (moh), and “second of minute” (som).
Dec handles differences from Zone 0 with time zone offsets. There is no time difference between Zone 0 and UNIX time. Dec applies a time zone offset 5 decidays to Julian dates because the Dec epoch occurred at midnight (0 decidays) and the Julian period started at noon (5 decidays).
To convert days to decidays, we multiply the tod by ten.
In Dec, days start at midnight, instead of noon, but have the same duration as Julian days: 86400 International System of Units (SI) seconds or 100000 Dec beats.
The duration of days in Dec and Julian dates The difference between the Dec epoch and the beginning of the Julian period is the only thing that distinguishes Julian date and a doe is its epoch.
In Coordinated Universal Time (UTC), days start at midnight, but the length of a day can vary due to leap seconds. In Dec, a day always has the same duration as a
When matching UTC is required, Dec avoids day length changes by adding leap seconds to a UTC time zone offset. According to Dec, the UTC+00:00 time zone does not have any leap seconds because its offset is 0. To indicate that a UTC time zone includes the 27 leap seconds that have been inserted into UTC so far, Dec appends :27 to its offset.
The Dec equivalent of the UTC+00:00:27 time zone is Zone 1/320.
In UTC, the length of Day 121 or Day 305 can vary due to leap seconds. T this offset as T To represent the 27 leap seconds that have been added to UTC so far, Dec uses an offset of 1/320 dd. Dec refers to the UTC time zone that has this offset as UTC+00:00:32.
either or secon Since 1972+121, 27 positive leap seconds have been added to UTC, and 0 negative leap seconds have been whave been in a Julian date or a doe i Dec does not allow The time in the ten Dec time zones never include
o depending on whether the leap second is p. Dec uses to represent leap seconds without changing the length of a day in the same way as leap days changes the length of a year. Instead of allowing the length of a day included, some UTC days will be 86401 or 86399 seconds long. So far, 27 positive leap seconds have been added, some UTC days will differ in length. includes of seconds, Dec uses beats. 100000 The programming languages that the Quarto FAQ refers to as principal languages supported by Quarto, Python (Quarto principal languages), R, Julia, and Observable JavaScript, do not include leap seconds in UTC time. Therefore, a day in Even though the Dec, UNIX, and UTC epochs all occurred at midnight, UTC time may be shifted in relation to Dec or UNIX time because of leap seconds. Since 1972+121, 27
We can make the time in any Dec time zone match UNIX by subtracting its time zone offset. The time in the Zone 0 Dec time zone always matches UNIX time. because neither includes leap seconds. Dec considers Coordinated Universal Time (UTC) +00:00 time zone to also match UNIX time. If there is a leap seconds are not included. Dec handles leap seconds like an additional time zone offset. UTC +00:00:00 with 27 leap seconds is referred to as Zone 1/320 in Dec and . which
To produce a Zone 0 tod from an hms triplet, we get the difference between the hms triplet and its UTC time zone offset, convert its components to either days or decidays, and sum the converted components.
and
\[\text{deciday} = \frac{\text{hour}}{2.4} + \frac{\text{minute}}{144} + \frac{\text{second}}{8640}\]
\[\text{day} = \frac{\text{hour}}{24} + \frac{\text{minute}}{1440} + \frac{\text{second}}{86400}\]
does not include leap seconds or a Coordinated Universal Time (UTC) time zone offset and
than +00 and +12 will not match the time in any of the ten Dec time zones. We can create a tod by summing the components of an hms triplet after
that were added a Coordinated Universal Time (UTC) +00:00 time zone. An hms triplet that includes leap seconds or a
To be compatible with Dec, a time zone offset must be equivalent to one of the ten positive single-digit integer deciday Dec offsets. The Zone 5 Dec time zone and UTC +12:00 time zone have equivalent offsets: 5 decidays = 12 hours. Likewise, the Zone 0 Dec time zone, UNIX time, and the UTC +00:00 time zone all have an offset of 0.
Apart from UTC +00:00 and +12:00, all of the other UTC time zones are incompatible with Dec. To find the deciday time difference \(\Delta\) between a UTC time zone and its closest Dec time zone, convert the offset of the UTC time zone to decidays and then calculate how much the converted offset changes after rounding it to the nearest deciday.
\[\Delta = \text{offset} - \lfloor\text{offset} + 0.5\rfloor\]
Repeating decimal numbers
Dec expresses incompatible offsets as positive or negative fractions or multi-digit integers. UTC offsets are either repeating or terminating decimal numbers. Dec displays repeating decimal offsets as fractions and terminating decimal offsets as integers. UTC +08:00, the most populous UTC time zone, has an offset of .3 days and is called Zone 1/3 in Dec. In contrast, UTC +03:00 has an offset of .125 days and is called Zone 125 in Dec.
Terminating decimal number offsets can be displayed by Dec provided there is enough space for the additional digits. Dec displays repeating decimal number offsets as fractions. The second most populous UTC time zone, UTC +05:30, translates to Zone 55/24 or Zone 2.2916 in Dec. The top six most populous UTC time zones all have positive offsets.
When it is midnight (tod=0) in Zone 0, it is noon (tod=5) in Zone 5 and the time in every other time zone is equal to it it is ~5.33 in Zone 10/3, ~4.29 in Zone 55/24, and
To obtain the Zone 0 time, we evaluate any Dec time as a math expression, add 10, and then get the remainder after dividing by 10 to make sure the result is less than 10 decidays.
The use of incompatible offsets makes mental calculations involving time zones much harder. Also, incompatible offsets are more difficult to display and read in the format of Dec times. Despite these clear disadvantages, Dec will attempt to parse, process, and display any time zone offset that is provided. Incompatible offset that are repeating decimal numbers can be displayed as fractions, truncated numbers
noon in UTC +08:00 could be written as 5.00-10/3 or 5.00-3.3.
Dec does not set a hard limit on the number of time zones, but negative time zones offsets to improve its compability with UTC.
Sorry if reading this takes a long time; I hope you don’t zone out!
If a Dec time zone offset is not specified, we can detected a time zone using Whether or not negative time zones offsets are worth the trouble is a matter of that the offer little to no benefit. The UTC -12:00 and +12:00 time zones and the Zone 5 and -5 Dec time zones all have the same time, but their dates do not match, because UTC -12:00 and Zone -5 are one day behind UTC +12:00 and Zone 5. Dec discourages the use of time zones with negative 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: width,
marginBottom: 50,
style: `overflow: visible;font-size:16px;`,
color: {scheme: "RdBu", className: "barPlotLegend"},
marginLeft: 75,
y: { label: null },
x: { grid: true, label: "Population (billions)", labelOffset: 38, transform: d => d / 1e9 },
marks: [
Plot.barX(sortedPop, {x: "pop", y: "Sign", fill: "Offset", stroke: "black", tip: true }),
]
})
Coordinated Universal Time (UTC)
The UTC -12:00 time zone contains only strict nature reserves and thus does not have any inhabited territory.
The remaining UTC time zones with negative offsets
If we disregard leap seconds, the +00 UTC time zone is synchronized with UNIX time and the Zone 0 Dec time zone. The +12 and -12 UTC offsets have the same time but their dates are one day apart.
We can honor this difference by translating +12 compatible 37 UTC offsets, only 3 are integers after they are converted to time zones. Conversion between Dec time zones and UTC offsets is inexact, because UTC offsets depend on geographic and political boundaries, whereas Dec time zones are determined solely by longitude.
because UNIX time, UTC+00:00, and Zone 0 are all synchronized.
Similarly, a UTC+00:00 hms triplet yields a Zone 5 tod.
UTC
Instead of passing an hms triplet and its UTC time zone offset to the UTC tod equation separately, we can subtract the offset from the triplet to produce a UTC+00:00 hms triplet. In other words, we can avoid converting time zone offsets if we always first shift the input hms triplet to UTC+00:00.
To obtain the Zone 0 time, we evaluate a Dec time as a math expression, add 10, and get the remainder after dividing by 10 to make sure the result is less than 10 decidays: ( + 10) mod 10 = .
Instead of passing both a UTC hms triplets and its UTC time zone offset to the UTC tod equation.
Instead of performing this calculation on both a UTC hms triplet and its UTC time zone offset, we should subtract the offset from the triplet.
equation should be a UTC+00:00 hms triplet so that we do not have to convert a UTC time zone offset to decidays.
When we add a UTC offset to a UTC hms triplet, with the resulting UTC+00:00 time.
The time in Zone 0 matches UTC time with the UTC+00:00 offset. UNIX time and three UTC offsets are called UTC+00:00, UTC+12:00, and UTC-12:00. Starting with a UTC+00:00 hms triplet results in Zone 0 tod.
only three out of the UTC time zones are synchronized with a Dec time zone it will most likely be easier to shift the hms triplet to will result in days instead of decidays if we divide by the most Dec and UTC time zones are not aligned. Whereas UNIX time is always synchronized with Zone 0 and UTC+00:00, This method ensure that the result matches
Drag the red⭕️circle across the meridians (vertical↕gray lines) on the map🗺️to see how changing time zones affects the time. Only the first digit of the Dec times shown above, the deciday, varies across time zones, because the 10 Dec time zones, numbered 0 through 9 on the map🗺️, are each 1 deciturn (dt) wide. Simply put, a deciturn of longitude translates into a deciday of time.
The leftmost vertical↕line on the map🗺️is Meridian 0, the Dec International Date Line and prime meridian, which cuts across the Atlantic Ocean through Iceland🇮🇸just West of Africa🌍and is the boundary between Zone 9 and Zone 0, the rightmost and leftmost Dec time zones on the map🗺️, respectively. Arranging Dec time zones from 0 to 9 yields a Pacific-centric map🗺️.
While only positive Dec time zones are shown on the map🗺️, every Dec time zone can also be expressed as a negative number. Each pair of time zone numbers produces the same Dec time, but result in Dec dates🗓️that are 1 day apart. Negative time zone numbers can be useful for getting Dec dates🗓️to match Gregorian calendar dates🗓️with negative UTC offsets.
If you know your longitude in degrees (°) or centiturns (ct), you can look up your Dec time zone (TZ) in the table below.
TZ + |
TZ - |
Start ° |
Mid ° |
End ° |
Start \(ct\) |
Mid \(ct\) |
End \(ct\) |
---|---|---|---|---|---|---|---|
9 | -1 | -54 | -36 | -18 | 90 | 95 | 100 |
8 | -2 | -90 | -72 | -54 | 80 | 85 | 90 |
7 | -3 | -126 | -108 | -90 | 70 | 75 | 80 |
6 | -4 | -162 | -144 | -126 | 60 | 65 | 70 |
5 | -5 | 162 | 180 | -162 | 50 | 55 | 60 |
4 | -6 | 126 | 144 | 162 | 40 | 45 | 50 |
3 | -7 | 90 | 108 | 126 | 30 | 35 | 40 |
2 | -8 | 54 | 72 | 90 | 20 | 25 | 30 |
1 | -9 | 18 | 36 | 54 | 10 | 15 | 20 |
0 | -10 | -18 | 0 | 18 | 0 | 5 | 10 |
Dec times in Zone 0 and 5 can be directly converted to and from UTC times with an offset of 0 and 12 hours, respectively. The other Dec time zones
Unit
Dec uses metric prefixes to create submultiples of a day that can naturally be combined together into a single decimal number. Conversion between decimal units is as simple as moving↔︎️or removing❌the decimal separator. In contrast, an hh:mm:ss time is a mixed-radix number, where hh is the base-12 or base-24 hour, mm is the base-60 minute, and ss is the base-60 second.
Prefix | Power | Day | hh:mm:ss.sss |
---|---|---|---|
0 | 1 | 24:00:00.000 | |
deci | -1 | .1 | 02:24:00.000 |
centi | -2 | .01 | 00:14:24.000 |
milli | -3 | .001 | 00:01:26.400 |
decimilli | -4 | .0001 | 00:00:08.640 |
centimilli | -5 | .00001 | 00:00:00.864 |
To convert the hour h, minute m, and second s into the deciday d, Dec uses the following equation: d = h ÷ 2.4 + m ÷ 144 + s ÷ 8640. The current equation values in Zone are: = ÷ 2.4 + ÷ 144 + ÷ 8640. Inversely, we can convert decidays into hours: h = d × 2.4, minutes: m = h mod 1 × 60, and seconds: s = m mod 1 × 60.
Instead of dealing with hours, minutes, and seconds, we can convert the UNIX timestamp u into the Dec time d+0. First, we divide u by 86400 to convert seconds to days, then isolate the decimal part of the quotient, and finally multiply by 10: d + 0 = u ÷ 86400 mod 1 × 10. The current values in this equation are + 0 = ÷ 86400 mod 1 × 10.
The concept of measuring time in decimal days is not novel. In the late 1700s, the French Republican calendar time system referred to decidays as decimal hours, centidays as décimes, millidays as decimal minutes, and centimillidays as decimal seconds. Similarly, Swatch Internet Time, a decimal time system introduced in 1998, uses the term “.beats” for millidays.
Swatch Internet Time differs from Dec in that it has no time zones and is obtained from the hours, minutes, and seconds of UTC+01:00. In contrast, the major innovations described in this article are the Dec time zone system and the simple equation for obtaining the Dec time in Zone 0 from a UNIX timestamp, but Dec has much more to offer than deciday times and zones.
Next
The next article in the Dec section of my site compares Dec to the ISO 8601 international standard for dates and times. Like ISO 8601, Dec allows for combined date and time representations that can be paired up to express time intervals. In Dec, the combination of a date and time is called a snap🫰and a time interval expressed as a pair of snaps is called a span🌈.
My ISO 8601 article is unique because it avoids the use of Observable in favor of leveraging Jupyter support in Quarto to make the code underlying Dec available in multiple programming languages. Observable is a great visualization tool but does not translate well into Jupyter notebooks. After the next article, I return to the use of Observable in my Dec snap🫰and span🌈articles.
%%{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 (
1.49504245e-5 +
-4.1898733e-2 * Math.cos(gamma) +
-2.9237036e-2 * Math.sin(gamma) +
-4.5694380e-2 * Math.cos(2 * gamma) +
5.17560175e-2 * Math.sin(2 * gamma)
)
}
dz = unix2dote(now)
decYear = ydz[0].toString().padStart(4, "0")
zeroDote = unix2dote(now, 0)[0]
zeroTime = zeroDote % 1
zeroDate = dote2date(zeroDote)
zeroYear = zeroDate[0]
zeroYda = zeroDate[1]
zeroDoy = Math.floor(zeroYda)
zeroIsLeap = isLeapYear(zeroYear)
zeroYdaHsl = textcolor(zeroYda.toFixed(5), d3.color(piecewiseColor(zeroYda / (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())
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) {
// turns: e=0, deciturns: e=1, etc.
return (degrees %= 360) / (360 / 10**e) % 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 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", "1.25")
.attr("stroke", "black");
/* Draw month ticks */
d3.range(12).map((m) => {
const d = new Date(date.getFullYear(), m, 1);
const angle = getSolarAngle(d);
solarSystem
.append("line")
.attr("x1", (solarSystemRadius + 9) * Math.sin(angle))
.attr("y1", (solarSystemRadius + 9) * stretch * Math.cos(angle))
.attr("x2", (solarSystemRadius - 9) * Math.sin(angle))
.attr("y2", (solarSystemRadius - 9) * stretch * Math.cos(angle))
.attr("stroke-width", "1.75")
.attr("stroke", "black");
const startMonthAngle = getSolarAngle(new Date(date.getFullYear(), m, 1));
solarSystem
.append("text")
.text(date2doty(d))
.attr("x", (solarSystemRadius + 18 - width / 50) * Math.sin(startMonthAngle) * 1.1)
.attr(
"y",
(solarSystemRadius + 2 - width / 3) * 6.2 * stretch * Math.cos(startMonthAngle) + Math.sign(Math.cos(startMonthAngle)) * 12
)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", fontSize * (width < 300 ? .9 : width < 400 ? .95 : width < 500 ? 1 : width < 600 ? 1.05 : width < 700 ? 1.1 : 1.2) + width / 100)
.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)`);
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", ".08").attr("fill", "none").attr("stroke", darkmode ? "#eee" : "#333").attr("id", "globeBorders");
path.pointRadius(5.5);
earth.append("path").attr("d", path({ type: "Point", coordinates: location })).attr("fill", "none").attr("stroke-width", .6).attr("stroke", "black");
path.pointRadius(4.5);
earth.append("path").attr("d", path({ type: "Point", coordinates: location })).attr("fill", "none").attr("stroke-width", 2.25).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 > 380 ? [3, 6, 9, 12, 15, 18, 21] : width > 90 ? [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 < 300 ? .7 : width < 325 ? .725 : width < 350 ? .75 : width < 375 ? .8 : width < 400 ? .9 : width < 450 ? .95 : width < 500 ? 1 : width < 600 ? 1.2 : width < 700 ? 1.3 : 1.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 < 400 ? .9 : width < 500 ? 1 : width < 600 ? 1.1 : width < 700 ? 1.2 : 1.3) * 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", margin.bottom + (width < 275 ? -102 : width < 300 ? -106 : width < 325 ? -108 : width < 350 ? -115 : width < 375 ? -120 : width < 400 ? -128 : width < 425 ? -130 : width < 450 ? -132 : width < 475 ? -136 : width < 500 ? -141 : width < 525 ? -142 : width < 550 ? -148 : width < 575 ? -149 : width < 600 ? -154 : width < 650 ? -157 : width < 675 ? -157 : width < 700 ? -161 : width < 725 ? -160 : width < 750 ? -165 : width < 775 ? -169 : -173))
.attr("y", margin.top - (width < 400 ? 33 : width < 500 ? 35 : width < 600 ? 37 : width < 700 ? 40 : 42))
.attr("text-anchor", "middle")
.attr("font-size", fontSize * (width < 300 ? .8 : width < 400 ? .9 : width < 500 ? 1 : width < 600 ? 1.1 : width < 700 ? 1.2 : 1.25) + width / 100)
.attr("font-family", "sans-serif")
.attr("transform", "rotate(-90)")
.attr("fill", "black");
root
.append("text")
.text("Day of year")
.attr("x", margin.left + width / 2 - (width < 500 ? 30 : width < 600 ? 28 : width < 700 ? 24 : 22))
.attr("y", margin.top + chartHeight / 4 + margin.bottom + (width < 275 ? 10 : width < 300 ? 18 : width < 325 ? 19 : width < 350 ? 22 : width < 375 ? 30 : width < 400 ? 37 : width < 450 ? 39 : width < 475 ? 40 : width < 500 ? 44 : width < 550 ? 50 : width < 600 ? 51 : width < 650 ? 50 : width < 700 ? 44 : width < 750 ? 40 : 43))
.attr("text-anchor", "middle")
.attr("font-size", fontSize * (width < 300 ? .95 : width < 400 ? 1 : width < 500 ? 1.05 : width < 600 ? 1.15 : width < 700 ? 1.2 : 1.25) + width / 100)
.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 ? 63 : width < 500 ? 62 : width < 600 ? 61 : 60)
.attr("y", width < 450 ? 8 : 1)
.attr("rx", 5)
.attr("width", fontSize * (width < 300 ? .95 : width < 400 ? 1 : width < 500 ? 1.05 : width < 600 ? 1.15 : width < 700 ? 1.2 : 1.25) + width / 100)
.attr("height", fontSize * (width < 300 ? .95 : width < 400 ? 1 : width < 500 ? 1.05 : width < 600 ? 1.15 : width < 700 ? 1.2 : 1.25) + width / 100)
.attr("fill", mapcolors.day);
legend
.append("text")
.attr("x", width < 400 ? 82 : width < 500 ? 85 : width < 600 ? 87 : 90)
.attr("y", width < 300 ? 22 : width < 400 ? 23 : width < 450 ? 24 : width < 500 ? 18 : width < 600 ? 20 : width < 700 ? 21 : 23)
.attr("font-size", fontSize * (width < 300 ? .95 : width < 400 ? 1 : width < 500 ? 1.05 : width < 600 ? 1.15 : width < 700 ? 1.2 : 1.25) + width / 100)
.attr("font-family", "sans-serif")
.text("Day");
legend
.append("rect")
.attr("x", width < 400 ? -10 : width < 500 ? -20 : width < 600 ? -40 : -39)
.attr("y", width < 450 ? 8 : 1)
.attr("rx", 5)
.attr("width", fontSize * (width < 300 ? .95 : width < 400 ? 1 : width < 500 ? 1.05 : width < 600 ? 1.15 : width < 700 ? 1.2 : 1.25) + width / 100)
.attr("height", fontSize * (width < 300 ? .95 : width < 400 ? 1 : width < 500 ? 1.05 : width < 600 ? 1.15 : width < 700 ? 1.2 : 1.25) + width / 100)
.attr("fill", mapcolors.night);
legend
.append("text")
.attr("x", width < 400 ? 11 : width < 500 ? 4 : width < 600 ? -12 : -9)
.attr("y", width < 300 ? 22 : width < 400 ? 23 : width < 450 ? 24 : width < 500 ? 18 : width < 600 ? 19 : width < 700 ? 21 : 23)
.attr("font-size", fontSize * (width < 300 ? .95 : width < 400 ? 1 : width < 500 ? 1.05 : width < 600 ? 1.15 : width < 700 ? 1.2 : 1.25) + width / 100)
.attr("font-family", "sans-serif")
.text("Night");
/* 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("x", xScale(date) - 4);
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", 4).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", 12)
.attr("fill", "red")
.attr("stroke-width", .6)
.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 = 0.65 * width;
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} : 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-width/14);
const canvas = context.canvas;
canvas.style.margin = `0px 0px -26px 0px`;
const projection = d3
.geoEquirectangular()
.precision(0.1)
.fitSize([width, height], { type: "Sphere" }).rotate([-153, 0]);
const path = d3.geoPath(projection, context).pointRadius(2.5);
formEl.append(canvas);
function draw() {
context.fillStyle = window.darkmode ? "black" : "white";
context.fillRect(0, 0, width, height);
context.beginPath(); path({type: "Sphere"});
context.fillStyle = window.darkmode ? "#007FFF" : mapcolors.ocean;
context.fill();
context.beginPath();
path(graticule);
context.lineWidth = 0.95;
context.strokeStyle = `#aaa`;
context.stroke();
context.beginPath();
path(land);
context.fillStyle = window.darkmode ? "#0808" : mapcolors.land;
context.fill();
context.beginPath();
path(countries);
context.lineWidth = .95;
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.stroke();
context.fillStyle = window.darkmode ? "#fff" : "#000";
context.font = width < 760 ? "14px serif" : width < 990 ? "17px serif" : "23px serif";
d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, 82 - (width < 500) * 8.8])));
d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, -66 + (width < 500) * 1.1])));
context.beginPath(), path(night), context.fillStyle = "rgba(0,0,255,0.1)", context.fill();
context.beginPath(); path.pointRadius(17); path({type: "Point", coordinates: sun}); context.strokeStyle = "#0008"; context.fillStyle = "#ff08"; context.lineWidth = 1; context.stroke(); context.fill();
if (lon != null && lat != null) {
path.pointRadius(17); context.strokeStyle = "black";
context.beginPath(); path({type: "Point", coordinates: [lon, lat]}); context.lineWidth = 1; context.stroke();
context.lineWidth = 6;
path.pointRadius(14); context.strokeStyle = "red";
context.beginPath(); path({type: "Point", coordinates: [lon, lat]}); context.stroke();
}
}
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();
const form = input({
type: "worldMapCoordinates",
title,
description,
display: v => (width > 300) ? html`<div style="width: ${width}px; white-space: nowrap; color: window.darkmode ? #fff : #000; text-align: center; font: ${width / 40}px monospace; position: relative; top: ${-16 - width / 50}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)
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
selSnap = 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(selTimeOne.toFixed(3).slice(2), 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())
selSnapDay = textcolor(selSnap.toFixed(3).padStart(6, "0"), d3.color(piecewiseColor(selSnap / 365)).formatHex())
selSnapDec = textcolor((selSnap * 10).toFixed(4).padStart(9, "0"), d3.color(piecewiseColor(selSnap / 365)).formatHex())
decZoneHsl = textcolor(selectedZone, d3.color(piecewiseColor(selectedZone / 10)).formatHex())
viewof selectedDate = Inputs.input(date2022)
viewof selectedHour = Inputs.input(date2022.getHours())
date2022 = new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().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();
sortedPop = populationByTimeZone.sort(
(a, b) => sortParams[1] ? sortFunc(a.number, b.number) : sortFunc(a.pop, b.pop)
)
sortFunc = sortParams[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)
dzOff = unix2dote(now, offset)
ydzOff = dote2date(...dzOff)
decTimeOff = ydzOff[1] % 1
decSignOff = offset < 0 ? "+" : "–"
decOffsetPosi = (offset + 10) % 10
decOffsetNega = decOffsetPosi - 10
decOffsetHsl0 = textcolor(Math.abs(offset), `hsl(${d3.hsl(piecewiseColor(decOffsetPosi / 10)).h}` + slStr)
decOffsetHsl1 = textcolor(Math.abs(offset), `hsl(${d3.hsl(piecewiseColor(decOffsetPosi / 10)).h}` + slStr)
decOffsetHsl2 = textcolor(offset, `hsl(${d3.hsl(piecewiseColor(decOffsetPosi / 10)).h}` + slStr)
decOffsetHslP = textcolor(decOffsetPosi, `hsl(${d3.hsl(piecewiseColor(decOffsetPosi / 10)).h}` + slStr)
decOffsetHslN = textcolor(decOffsetNega, `hsl(${d3.hsl(piecewiseColor(decOffsetPosi / 10)).h}` + slStr)
decTimeOffHsl0 = textcolor((decTimeOff * 10).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeOff)).h}` + slStr)
decTimeOffHsl1 = textcolor((decTimeOff * 10).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeOff)).h}` + slStr)
decTimeOffHsl2 = textcolor((decTimeOff * 10).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeOff)).h}` + slStr)
decTimeOffHsl3 = textcolor((decTimeOff * 10).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeOff)).h}` + slStr)
decTimeOffLeapNum = offset * 320 + 1
decTimeOffLeapTodHsl = textcolor((decTimeOff * 10 + 1 / 320).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeOff)).h}` + slStr)
decTimeOffLeapTzoHsl = textcolor(`${decTimeOffLeapNum}/320`, `hsl(${d3.hsl(piecewiseColor((decTimeOffLeapNum / 320 + 10) % 10 / 10)).h}` + slStr)
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")
leapDecidaysHsl = textcolor(frac(leapCount).div(8640).toFraction(), leapColor)
longInputHsl = textcolor(longInput, d3.color(piecewiseColor(longInput / 10)).formatHex())
eot = getEot(leapSecondDate)
astTzo = eot + longInput
astTzoHsl = textcolor(astTzo.toFixed(4), d3.color(piecewiseColor(astTzo / 10)).formatHex())
solarNoon = (5 - astTzo + 10) % 10
solarNoonHsl = textcolor(solarNoon.toFixed(4), d3.color(piecewiseColor(solarNoon / 10)).formatHex())
solarDiff = zeroTime - solarNoon
solarSign = solarDiff < 0 ? "-" : "+"
solarDiffHsl = textcolor(Math.abs(solarDiff).toFixed(4), d3.color(piecewiseColor((solarDiff + 10) % 10 / 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;
}
}
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;
}
</style>
`
Reuse
Citation
@online{laptev2024,
author = {Laptev, Martin},
title = {Dec {Time}},
date = {2024},
urldate = {2024},
url = {https://maptv.github.io/dec/time},
langid = {en}
}