%%{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.
Author
Published
2025+121
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.
Time of day (tod)
Dec times consist of a time-of-day (tod) and a time zone: –. The tod denoted by the red🔴dot is days or decidays. The day-of-year (doy) indicated by the red—line is . Together, a doy and tod form a floating🛟snap🫰. Like tods, floating🛟snaps🫰can be expressed in days, , or decidays: .
A floating🛟snap🫰identifies a day in an unspecified year with its first three digits, classifies that day as a work or rest day with its third digit, and specifies an point in that day with its remaining digits, thus serving the same purposes as a Gregorian calendar month and day-of-month (dom) pair, a day-of-week (dow), and an hour-minute-second (hms) triplet.
Day of era (doe)
To turn a floating🛟snap🫰into a tod, we keep the remainder after dividing days by one or decidays by ten. We can use this same approach to obtain a tod from a day-of-era (doe), mod 1 = , or deciday-of-era (ddoe): mod 10 = . The current doe tells us how many days have passed since the Dec epoch.
Similarly, UNIX time counts the seconds since the UNIX epoch. To get a deciday tod from UNIX time, we divide by the number of seconds in a deciday and then keep the remainder after dividing by ten. If we want to convert seconds to days instead of decidays, we divide by the number of seconds in a day and then keep the remainder after dividing by one.
\[\text{deciday} = \text{unix} \div 8640 \text{ mod } 10\]
\[\text{day} = \text{unix} \div 86400 \text{ mod } 1\]
Hour minute second
We can also create a tod by summing the components of an hms triplet after converting them to either fractional days or decidays. Unlike UNIX time, hms triplets may include leap seconds and a Coordinated Universal Time (UTC) time zone offset. Dec does not acknowledge leap seconds and is only compatible with UTC three offsets: +00, +12, and -12.
\[\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}\]
Coordinated Universal Time (UTC)
because UNIX time, UTC+00:00, and Zone 0 are all synchronized.
Zone 0 UTC+00:00
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
Time zone offset
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.
There are 37 UTC offsets, but only 10 Dec 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. 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 will differ from the closest UTC time by 8.3, 16.6, 25, 33.3, 41.6 or 50 millidays. To find the difference δ, convert the UTC offset hours oH and minutes oM into deciturns of the longitude λ and then subtract the Dec zone number z:
\[\lambda = o_H \div 2.4 + o_M \div 144\]
\[z = \lfloor\lambda + 1 \div 2\rfloor\]
\[\delta = \lambda - z\]
We can avoid dealing with the time zone difference by shifting the time zone so that we always convert between Zone 0 and UTC+00:00 or Zone 5 and UTC+12: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 = .
Bad Pun Alert
Sorry if reading this takes a long time; I hope you don’t zone out!
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]}
dz = unix2dote(now)
decYear = ydz[0].toString().padStart(4, "0")
zeroDote = unix2dote(now, 0)[0]
zeroTime = zeroDote % 1
zeroDate = dote2date(zeroDote)
zeroYear = zeroDate[0]
zeroDoty = Math.floor(zeroDate[1])
zeroTimeHsl0 = textcolor(zeroTime.toFixed(5).slice(1), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroTimeHsl1 = textcolor((zeroTime * 10).toFixed(4), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroTimeHsl2 = textcolor(zeroTime.toFixed(5).slice(2), d3.color(piecewiseColor(zeroTime)).formatHex())
zeroDotyHsl = textcolor(zeroDoty.toString().padStart(3, "0"), d3.color(piecewiseColor(zeroDoty / (365 + zeroIsLeap))).formatHex())
zeroYearHsl = textcolor(zeroYear, d3.color(piecewiseColor(zeroYear % 1000 / 1000)).formatHex())
zeroIsLeap = zeroYear % 4 == 0 && zeroYear % 100 != 0 || zeroYear % 400 == 0;
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>`;
}
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])
decDateHsl = textcolor(decDate.toString().padStart(3, "0"), d3.color(piecewiseColor(decDate / (365 + isLeapYear))).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(5).slice(1), 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(5).padStart(9, "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())
isLeapYear = decYear % 4 == 0 && decYear % 100 != 0 || decYear % 400 == 0;
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}));
}
hD306 = d3.hsl(piecewiseColor(306 / 365)).h
hues = Object.fromEntries([
0.969,
].map(i => [i, d3.hsl(piecewiseColor(i)).h])
);
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;
}
.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;
}
.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;
}
.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;
}
.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;
}
.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
BibTeX citation:
@online{laptev2024,
author = {Laptev, Martin},
title = {Dec {Time}},
date = {2024},
urldate = {2024},
url = {https://maptv.github.io/dec/time},
langid = {en}
}
For attribution, please cite this work as:
Laptev, Martin. 2024. “Dec Time.” 2024. https://maptv.github.io/dec/time.