%%{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+200
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}\]
\[\lfloor\text{yda}\rfloor = \text{doy}\]
\[\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 mod } 1 = \text{eda 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
Universal time offset
The Global Positioning System, BeiDou, and Galileo global navigation satellite systems along with most — if not all — programming languages do not account for leap seconds, which appears to be for the best given that leap seconds will be abolished by 2035. The goal of leap seconds is to keep UTC within 25/24 centimillidays (cmds) of Universal Time (UT).
Instead of leap seconds, Dec matches UT using a “universal time offset” (uto). With the leap second insertion dates provided by the International Earth Rotation and Reference Systems Service, we can approximate the uto that yields UT when added to the Zone 0 tod on the Dec date chosen by the range🎚️inputs below: ÷ 8640 = .
Rounded offset decimal
Of the twenty eight utos that can be shown in the equation above, one is an integer, one is a terminating decimal, and the rest are repeating decimals. To express a repeating decimal uto, Dec uses an irreducible fraction that is called an “exact offset fraction” (eof) when by itself or a “rounding error fraction” (ref) if it follows a “rounded offset decimal” (rod).
In the equation below, the uto is the minuend, the rod is the subtrahend, and the ref is the difference. Dec uses the term minuend expansion to describe the replacement of a minuend with a subtrahend and a difference. By replacing a repeating decimal uto with a rod and a ref, we can show the initial digits of the uto as a decimal and the rest as a fraction.
\[\text{uto} - \text{rod} = \text{ref}\]
Use the first three range🎚️inputs below to select an hms triplet to be converted to decidays, plugged into the equation above as the uto, rounded to the number of digits chosen by the fourth range🎚️input, and inserted into the equation as the rod. Once the left-hand side of the equation is complete, we can solve it to get the ref: – = .
Time zone offset
In Dec, a uto can be any type of number, a “time zone offset” (tzo) is an integer, a “solar time offset” (sto) is a terminating decimal, and an eof is a repeating decimal. If we derived deciday offsets from all 86400 of the hms triplets that can be selected by the range🎚️inputs above, we would have 10 tzos, 3190 stos, and 83200 eofs or rod and ref pairs.
Coordinated Universal Time (UTC)
When we do the same to the 38 UTC offsets, we get only eofs or rod and ref pairs unless the number of leaps seconds included is zero or a multiple of 27. If the leap second count is zero or a multiple of 8640, we will get 3 tzos, 9 stos, and 26 eofs or rod and ref pairs. The 3 tzos will be stos if the number of leap seconds is a multiple of 27 but not 8640.
There are 14 negative and 24 positive UTC offsets. The UTC time zone with the most negative offset is completely uninhabited. The bar📊chart below visualizes Socioeconomic Data and Applications Center data from 2020 regarding the population of each UTC time zone. The vast majority of all people live in UTC time zones with positive offsets.
// https://observablehq.com/@mattdzugan/population-by-time-zone-creating-a-dataset
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 }),
]
})
Negative UTC offsets only exist in the Americas and islands in the Atlantic and Pacific Oceans. Therefore, the bar📊chart above is essentially comparing the Americas to the rest of the world. According to 2021 United Nations Department of Economic and Social Affairs data, about one billion out of a total of almost eight billion people live in the Americas.
Whenever a negative offset is associated with a Dec date, a tod, or both a date and a tod, Dec will add one day to the date and ten decidays to the offset without modifying the tod. This typically occurs after the conversion of an hms triplet to a tod or a “year month day” (ymd) triplet to a Dec date. As a result, all Dec dates and tods have positive offsets.
Dec will not change a negative offset or its associated doy if the result of adding one day to the doy is uncertain. This uncertainly can only exist if we do not know whether a doy that is equal to 364 belongs to a common or leap year. The day after Day 364 of a common year is Day 0 of the subsequent year. In a leap year, Day 364 precedes Day 365.
Day of week
Even though it has no effect on the tod, adding one day to the doy also increments the “day of month” (dom) and “day of week” (dow) shown by Dec. The table below shows how someone accustomed to a negative offset could intrepret Dec dow numbers. From the perspective of a negative offset user, the dom and dow in Dec will be one day ahead.
Saturday | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday |
---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 |
-7 | -6 | -5 | -4 | -3 | -2 | -1 |
The one day difference between positive and negative offsets may make Dec dow numbers more intuitive than POSIX dow numbers for people who consider Sunday to be the first dow. According to the Common Locale Data Repository and a 2023 population ranking, over 89% of people in the Americas live in a country that starts the week on Sunday.
Longitude and offsets
In Dec, offsets are closely related to longitude. Dec measures longitude in parallels (λ) or submultiples of λ like deciparallels (dλ). A tzo is essentially a dλ longitude that had its decimal part removed via rounding, flooring, truncation, or ceiling. Whereas tzos have one digit, deciday tods and stos typically have up to four digits after the decimal separator.
The fourth digit in the decimal part of any current deciday tod increments 105 times per day, 100 times per milliday, or once per beat (iob), which is the lower bound of the normal resting heart rate of an adult. For everyday life, we should limit the length of tods to the three digits needed to show millidays or the five digits required to display beats (b).
When the current tod is shown with seven digits, the sixth digit changes too quickly to be read out loud and seventh changes so fast that it appears as a blur. Near the Equator, a longitude that has seven digits is accurate to within about ten zems (z) or four meters, which is roughly the length of a subcompact car or the width of a U-shaped living room layout.
The Equator is approximately (~) 103 millitaurs (mc), ~4 × 104 kilometers (km), or ~105 kilozems (kz) long. If we are near the Equator and move 1 mc, 40 km, or 100 kz to the East or West, our sto will change by ~1 md, ~1.44 minutes, or ~100 b and our longitude will shift by ~0.36 degrees, ~1 mλ, ~21.6 arcminutes, or ~100 arcbeats (ab).
For precise geopositioning, it may be helpful to show geographic coordinates in submultiples of ab, but we are unlikely to benefit from units smaller than md when displaying the solar time or estimates of what the tod will be when the Sun rises, reaches its zenith, or sets on a given day. By default, Dec uses three digits to show each solar time and sto.
Equation of time
The two types of solar time are “mean solar time” (mst) and “apparent solar time” (ast). To calculate mst, we keep only the decimal part of the sum of 0.95, the Zone 0 tod measured in days, and our longitude measured in λ. If we want ast instead of mst, the sum needs to include the result of plugging the “time of year” (toy) into the equation of time (eot).
\[\text{toy} = \text{yda} \div \text{n}\]
\[\text{mst} = (0.95 + \text{tod} + \lambda) \text{ mod } 1\]
\[\text{ast} = (0.95 + \text{tod} + \lambda + \text{eot(toy)}) \text{ mod } 1\]
To obtain the toy, we divide the yda by the number of days in the year (n). If we use trigonometric functions that are designed to work with radians, we will have to multiply the toy by \(2\pi\) or \(\tau\). We do not need to modify the toy before passing it to the eot() function defined below because its trigonometric functions expect turns instead of radians.
\[\begin{split} \text{eot(toy)} & = \beta_0 \\ & + \beta_1 \times \text{costau(toy)} \\ & + \beta_2 \times \text{costau(2} \times \text{toy)} \\ & + \beta_3 \times \text{sintau(toy)} \\ & + \beta_4 \times \text{sintau(2} \times \text{toy)} \end{split}\]
(mpf('1.0'), mpf('0.0'))
(mpf('0.0'), mpf('-1.0'))
(1.0, 6.123233995736766e-17)
(1.2246467991473532e-16, -1.0)
The code above compares two functions which return both the sine and cosine of their input. The sincos() function assumes it will receive radians and the sincostau() works with turns. In general, working with turns is more convenient and intuitive. The examples above demonstrate that using turns instead of radians can yield more accurate results.
The National Oceanic and Atmospheric Administration (NOAA) provides yearly and daily eot values. To create a Dec version of the eot, we can convert the yearly NOAA eot values from minutes to centidays, combine dates and times into ydas, sort by yda, fit a curve to the sorted values, and then use the fitted model coefficients in the NOAA General Solar Position Calculations:
{0: 0.00014949317633417003,
1: -0.4164553809136968,
2: -0.2959657573651431,
3: -0.46578504463469605,
4: 0.5096179567359089}
// 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: "centiday eot(toy)"},
x: {label: "day of year"},
marks: [
Plot.line(samples, {
strokeWidth: 4,
stroke: "steelblue",
tip: true
}),
Plot.ruleX([-366]),
Plot.ruleY([0]),
Plot.axisX({ y: 0 }),
Plot.axisY({ x: -366 })
]
});
return plot;
}
The line📈chart above uses centiday eot(toy) values to show fewer digits on its y-axis. For time calculations, Dec always uses units that do not have metric prefixes. To calculate the tod of solar noon, the moment when the sun reaches its highest point in the sky, we keep only the decimal part after subtracting a longitude measured in λ and an eot(toy) value from the sum of 9.55 and a tzo.
\[\text{noon} = (9.55 + \text{tzo} - \lambda - \text{eot(toy)}) \mod 1\]
Solar declination angle
Once we have eot(toy), we can find the Zone 0 tod of solar noon by subtracting a longitude measured in λ and eot(toy) from 9.55, a constant which shifts . 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 same math for the eot and sda, even though the NOAA General Solar Position Calculations propose the more complex model shown below.
\[\begin{split} \text{sda(toy)} & = \beta_0 \\ & + \beta_1 \times \text{costau(toy)} \\ & + \beta_2 \times \text{sintau(toy)} \\ & + \beta_3 \times \text{costau(2} \times \text{toy)} \\ & + \beta_4 \times \text{sintau(2} \times \text{toy)} \\ & + \beta_5 \times \text{costau(3} \times \text{toy)} \\ & + \beta_6 \times \text{sintau(3} \times \text{toy)} \\ \end{split}\]
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\]
The sunrise and sunset times in the equations above depend on the time zone of solar noon. To select a time zone may be strange to consider The range🎚️inputs below set the geographic coordinates that are used for the equations above. The line chart beneath the inputs visualizes sunrise and sunset for every doy, similar to the daylight area chart beneath the map🗺above. The main difference between the charts is that the area chart shows local tods and thus is independent of longitude, whereas the line chart displays tzos and therefore changes with longitude.
To find the UTC tzo of a given longitude and latitude, we could use an application programming interface (API) or a database. If we only have longitude, we need to first round degrees to zero or the nearest multiple of fifteen for whole hour tzos, 7.5 for half hour tzos, or 3.75 for quarter hour tzos and then divide by fifteen to convert degrees to hours.
Next
The next article in the Dec section of my site shows how we can combine Dec dates and times into Dec Dec snaps🫰, which are analogous to the combined date and time representations in the ISO 8601 international standard for dates and times. The final article in the Dec section demonstrates how Dec dates, times, and snaps🫰can be paired up to express time intervals called Dec spans🌈.
%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '32px'}}}%% flowchart LR A[Dec]-->B[date]-->C[time]-->D[snap]-->E[span] click A "/dec" click B "/dec/date" click C "/dec/time" click D "/dec/snap" click E "/dec/span"
Cite
Please spread the good word about Dec using the citation information at the bottom of this article. You may also want to cite the Observable notebooks that I adapted into the clock🕓, bar📊chart, map🗺️, and daylight☀️plot visualizations in this article or the 2014 blog post which proposed a system of 20 decimal time zones, each 5 centidays wide, based on the Greenwich Meridian:
- Pearson, Tom. 2013+124. “Simple D3 clock.” +. https://observablehq.com/@d3/simple-clock.
- Heyen, Frank. 2021+246. “BarChart Clock.” +. https://observablehq.com/@fheyen/barchart-clock.
- Johnson, Ian. 2021+090. “Draggable World Map Coordinates Input.” +. https://observablehq.com/@enjalot/draggable-world-map-coordinates-input.
- Bridges, Dan. 2021+311. “Visualizing Seasonal Daylight.” +. https://observablehq.com/@dbridges/visualizing-seasonal-daylight.
- Clements, John. 2014+091, “Decimal Time Zones.” +. https://www.brinckerhoff.org/blog/2014/05/31/decimal-time-zones.
function unix2dote(unix, zone, offset = 719468) {
return [(unix ?? Date.now()) / 86400000 + (
zone = zone ?? -Math.round(
(new Date).getTimezoneOffset() / 144)
) / 10 + offset, zone]
}
function unix2dote1(unix, zone, offset = 719468) {
return [
(unix ?? Date.now()) / 86400000
+ (zone = zone ?? (10 - Math.round(
(new Date).getTimezoneOffset() / 144)) % 10
) / 10 + offset, zone]
}
function dote2date(dote, zone = 0) {
const cote = Math.floor((
dote >= 0 ? dote
: dote - 146096
) / 146097),
dotc = dote - cote * 146097,
yotc = Math.floor((dotc
- Math.floor(dotc / 1460)
+ Math.floor(dotc / 36524)
- Math.floor(dotc / 146096)
) / 365);
return [
yotc + cote * 400,
dotc - (yotc * 365
+ Math.floor(yotc / 4)
- Math.floor(yotc / 100)
), zone]}
function getEot(day) {
const gamma = 2 * Math.PI * day / 365
return (
1.4949317633417003e-4 +
-0.4164553809136968 * Math.cos(gamma) +
-0.2959657573651431 * Math.sin(gamma) +
-0.46578504463469605 * Math.cos(2 * gamma) +
0.5096179567359089 * 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())
zeroTimeHsl4 = textcolor((zeroTime * 10).toFixed(4), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroTimeHsl5 = textcolor((zeroTime * 10).toFixed(5), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroDotyHsl = textcolor(zeroDoy.toString().padStart(3, "0"), d3.color(piecewiseColor(zeroDoy / (365 + zeroIsLeap))).formatHex())
zeroDoteHsl = textcolor(zeroDote.toFixed(5), d3.color(piecewiseColor(zeroDote % 1)).formatHex())
zeroYearHsl = textcolor(zeroYear, d3.color(piecewiseColor(zeroYear % 1000 / 1000)).formatHex())
browserDote = unix2dote(now)
browserTime = browserDote[0] % 1 * 10
browserZone = browserDote[1]
browserSign = browserZone > 0 ? "-" : "+"
zone0time = (browserTime - browserZone + 10) % 10
hours = browserTime * 2.4
minutes = hours % 1 * 60
seconds = minutes % 1 * 60
selectedDote = unix2dote(now, long2zone(location[0]))
selectedExact = selectedDote[0] % 1
selectedExactN = (1 - selectedExact) % 1
selectedZone = selectedDote[1]
ydz = dote2date(...selectedDote)
decDate = Math.floor(ydz[1])
decTime = (selectedExact * 10).toFixed(4)
decTimeN = (selectedExactN * 10).toFixed(4)
barDD = decTime[0]
barDDN = decTimeN[0]
barMils = decTime.slice(2, 4)
barMilsN = decTimeN.slice(2, 4)
barBeats = decTime.slice(4, 6)
barBeatsN = decTimeN.slice(4, 6)
function lati2turn(degrees = -180, e = 3) {
// 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;
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")
leapFrac = frac(leapCount).div(8640).toFraction()
leapTzoHsl0 = textcolor(leapFrac, leapColor)
leapTzoHsl1 = textcolor(leapFrac, leapColor)
leapTzoHsl2 = textcolor(leapFrac, leapColor)
hmsTzo = frac(hmsinput[0]).div(24).add(hmsinput[1], 1440).add(hmsinput[2], 86400).mod(1)
hmsTzox10 = hmsTzo.mul(10)
pluralx10 = hmsTzox10.equals(1) ? "" : "s"
hmsRounded = hmsTzox10.round(digits - 1)
hmsRef = hmsRounded.sub(hmsTzox10)
hmsRefHsl = textcolor(hmsRef.abs().toFraction(), d3.color(piecewiseColor(Number(hmsRef.div(10)))).formatHex())
hmsRefSign = hmsRef.lt(0) ? "-" : "+"
hmsTzoHsl = textcolor(hmsTzox10.toFraction(), d3.color(piecewiseColor(Number(hmsTzo))).formatHex())
hmsRodHsl = textcolor(hmsRounded, d3.color(piecewiseColor(Number(hmsRounded.div(10)))).formatHex())
hmsTod = (zeroTime + hmsTzo) % 1
hmsTodHsl = textcolor((hmsTod * 10).toFixed(4), d3.color(piecewiseColor(hmsTod)).formatHex())
leapTod = (zeroTime + leapCount / 86400) % 1
leapDecidayTod = (leapTod * 10).toFixed(4)
leapTodHsl0 = textcolor(leapDecidayTod, d3.color(piecewiseColor(leapTod)).formatHex())
leapTodHsl1 = textcolor(leapDecidayTod, d3.color(piecewiseColor(leapTod)).formatHex())
longInputHsl = textcolor(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}
}