%%{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+209
My website provides many examples of the Quarto publishing and the Dec measurement systems in action. I leverage Quarto support for the Observable data analysis and visualization system to create animated and interactive graphics like the bar📊charts clocks🕓, solar☀️terminator map🗺, Earth🌍orbit diagram, and daylight area chart below.
Dec times are measured in fractional days, often using metric prefixes like deci, centi, or milli. The top, middle, and bottom bars📊indicate the decidays, millidays, and centimillidays, respectively, of the time since the start, +, or until the end, -, of the day in the Dec time zone, , at the location of the red⭕️circle on the map🗺️beneath the bars📊.
To rotate the globe🌐in the Earth🌏orbit diagram, drag the red⭕️circle horizontally↔︎️or slide the red🔴dot on the daylight area chart vertically↕. The red⭕️circle’s vertical↕position determines the yearly daylight pattern visualized by the area chart. Shift the red—line on the area chart horizontally↔︎️to move the globe🌐along the ellipse of the Earth🌎orbit.
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
// https://observablehq.com/@dbridges/visualizing-seasonal-daylight
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 : width < 800 ? .81 : width < 900 ? .80 : width < 1000 ? .76 : width < 1100 ? .74 : width < 1200 ? .72 : .7)));
svg.style("user-select", "none").style("-webkit-user-select", "none").attr("id", "daylightapp");
const margin = {top: 0, left: 16, right: 16, bottom: 0, inner: 32};
const contentWidth = width - margin.left - margin.right - margin.inner;
const columnWidth = contentWidth / 2;
let selection = {
date: this.value != null ? this.value.date : new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().getHours()),
hour: this.value != null ? this.value.hour : new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().getHours()).getHours()
}
const renderPlot = () => {
svg.selectAll("#plot *").remove();
svg.select("#plot").call(daylightPlot, {
vizwidth: columnWidth / (width < 300 ? 1 : 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 < 600 ? 1.54 : width < 800 ? 1.54 : width < 900 ? 1.54 : width < 1000 ? 1.54 : width < 1100 ? 1.54 : width < 1200 ? 1.54 : width < 1300 ? 1.54 : width < 1400 ? 1.54 : 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;
this.value = selection;
set(viewof selectedDate, selection.date);
set(viewof selectedHour, selection.hour);
if (forceRender) {
renderPlot();
renderSolarSystem();
// renderGlobe();
} else if (prev.hour !== selection.hour || prev.date !== selection.date) {
renderSolarSystem();
// renderGlobe();
}
}
const plot = svg.append("g")
.attr("id", "plot")
.attr("transform", `translate(${margin.left + (width < 500 ? 26 : width < 600 ? 24 : width < 700 ? 22 : width < 800 ? 20 : width < 900 ? 18 : width < 950 ? 16 : width < 1000 ? 15 : width < 1100 ? 22 : width < 1200 ? 20 : width < 1300 ? 15 : width < 1400 ? 10 : width < 1500 ? 5 : 0)}, ${width < 500 ? -4 : width < 600 ? -12 : width < 700 ? -20 : width < 800 ? -28 : width < 900 ? -36 : width < 950 ? -44 : width < 1000 ? -48 : width < 1100 ? -52 : width < 1200 ? -54 : width < 1300 ? -56 : width < 1400 ? -60 : width < 1450 ? -62 : -76})`);
svg.append("g")
.attr("id", "solar-system")
.attr("transform", `translate(${margin.left + 12}, ${margin.top + height / 1.6 + 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();
}
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 replace an yda with 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.103551
(as.numeric(as.POSIXct(hms)) / 86400) %% 1
[1] 0.103551
The equations below convert UNIX time or a Zone 0 tod into the three components of an hms triplet: the “hour of day” (hod), “minute of hour” (moh), and “second of minute” (som), using a “daily second aggregate” (dsa) and “hourly second aggregate” (hsa). While both count seconds, dsas start at midnight and hsas begin at the top of the hour.
\[\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] 2 29 6
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 (md) or the five digits required to display beats (b).
When the current tod has seven digits, the sixth digit changes too quickly to be read out loud and the seventh changes so fast that it appears as a blur. Near the Equator, a longitude that has seven digits is accurate to within about ten zems (z) or four meters, which is roughly the length of a subcompact car or the width of a U-shaped living room layout.
The Equator is approximately (~) 103 millitaurs (mc), ~4 × 104 kilometers (km), or ~105 kilozems (kz) long. If we 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 values below are the coefficients of a model adapted from the National Oceanic and Atmospheric Administration (NOAA) General Solar Position Calculations. Before fitting the model to NOAA yearly solar data, we need to convert eot(toy) values from minutes to centidays, combine ymd triplet dates and hms triplet times into ydas, and sort by yda.
{0: 0.0011114386869002235,
1: -4.155124400404918,
2: -2.9710873225303756,
3: -4.635570414590801,
4: 5.116176940364264}
// https://observablehq.com/@mcmcclur/plot-for-mathematicians
{
let w = 800;
let h = 0.625 * w;
let samples = build_samples(
getEot, -366, 365, { N: 150 }
);
let plot = Plot.plot({
marginLeft: 60,
width: w,
height: h,
style: "font-size:19;",
y: {label: "milliday eot(toy)"},
x: {label: "day of year"},
marks: [
Plot.line(samples, {
strokeWidth: 4,
stroke: "steelblue",
tip: true
}),
Plot.ruleX([-366]),
Plot.ruleY([0]),
Plot.axisX({ y: 0 }),
Plot.axisY({ x: -366 })
]
});
return plot;
}
The line📈chart above uses md to display eot(toy) values as integers. There is little difference between mst and ast around Days 45, 103, 184, and 299. The difference ranges from about -9.8 on Day 244 to around 11.4 md on Day 350. We can calculate mst with just a tod and a longitude and we will be off by at most about a centiday compared to ast.
Apart from turning a mst into an ast, we can also use eot to more accurately estimate the tods of solar noon, sunrise, and sunset. The equation below creates a solar noon tod measured in days by adding 9.55 to a longitude measured in λ, subtracting a tzo and a eot(toy) value that are both measured in days, and keeping only the decimal part of the result.
\[\text{noon} = (9.55 + \text{tzo} - \lambda - \text{eot(toy)) mod 1}\]
Cambridge and Cambridge
To compare the solar noon tod in two cities, we can plug in the longitude of each city into the equation above. If all the variables in the equation other than longitude are set to zero, the result is almost five decidays for Cambridge, England in the United Kingdom and nearly seven decidays for Cambridge, Massachusetts in the United States.
\[4.99635 = (9.55 - 0.050365) \text{ mod 1} \times 10\]
\[6.97516 = (9.55 - 0.852484) \text{ mod 1} \times 10\]
The two homonymous cities are about two dλ apart and thus will always differ by around two decidays in solar time, regardless of what time zone we use as our frame of reference. England is in Zone 0 and Massachusetts is in Zone 8. If we change the tzo from zero to eight decidays, the solar noon tod for each city will be two decidays earlier.
\[2.99635 = (9.55 + 0.8 - 0.050365) \text{ mod 1} \times 10\]
\[4.97516 = (9.55 + 0.8 - 0.852484) \text{ mod 1} \times 10\]
Solar declination angle
The duration of daytime is the difference between the sunset and sunrise tods. To calculate the daytime duration, we only need a latitude and a “solar declination angle” (sda). For simplicity, we can use the same model 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}\]
Dec measures latitude in turns called meridians (φ). Like λ, we can pass φ directly into trigonometric functions that accept turns. If the “solar zenith angle” (sza) in the equation below is 0.252314 turns, the equation will give us a “solar hour angle” (sha) that we can use to find out how long the Sun will shine or when it will rise or set on a given doy.
\[\text{sha} = \arccos\left( \frac{\text{costau(sza)} - \text{sintau($\phi$)} \times \text{sintau(sda)}}{\text{costau($\phi$)} \times \text{costau(sda)}} \right)\]
Before being plugged into the sha equation above, the mφ latitude selected by the first range input below is divided by 1000 to convert it to φ and the doy chosen by the second range input is turned into a toy and then into an sda. When the sza is 0.252314, we can divide the result of the sha equation by \(\pi\) to obtain the daytime duration: .
\[\text{sunset} - \text{sunrise} = \text{sha} \div \pi\]
Like other trigonometric functions, the arcosine function returns radians. We can divide by \(\tau\) to convert radians to turns. After converting the sha to turns, we can sum it with the solar noon to get the sunset tod or subtract it from the solar noon tod to obtain the sunrise tod. The sunrise and sunset tods are equidistant from the solar noon tod.
\[\text{sunset} = \text{noon} + \text{sha} \div \tau\]
\[\text{sunrise} = \text{noon} - \text{sha} \div \tau\]
Before we insert the latitude selected by the second range input, we convert mφ to φ.
\[\text{sha} \div \tau = \text{noon} - \text{sunrise} = \text{sunset} - \text{noon}\]
When we divide the sha by \(\tau\), we get the difference between the solar noon and sunrise tods or the sunset and solar noon tods.
Sunrise and sunset
Solar hour angle
The first range input above sets the doy that will be used to obtain the sda that along with the φ selected by the second range input will be plugged into the sha equation above. Once we have the sha, we can double it to get the daylight duration in negative days, add it to the solar noon tod to get the sunrise tod, or subtract it from the solar noon tod to get the sunset tod.
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} = \left|\text{sha} \times 2\right|\]
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 snaps🫰, which are analogous to the combined date and time representations in the ISO 8601 international standard for dates and times. The final article in the Dec section demonstrates how Dec dates, times, and snaps🫰can be paired up to express time intervals called Dec spans🌈.
%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '32px'}}}%% flowchart LR A[Dec]-->B[date]-->C[time]-->D[snap]-->E[span] click A "/dec" click B "/dec/date" click C "/dec/time" click D "/dec/snap" click E "/dec/span"
Cite
Please spread the good word about Dec using the citation information at the bottom of this article. You may also want to cite the Observable notebooks that I adapted into the clock🕓, bar📊chart, map🗺️, and daylight☀️plot visualizations in this article or the 2014 blog post which proposed a system of 20 decimal time zones, each 5 centidays wide, based on the Greenwich Meridian:
- Pearson, Tom. 2013+124. “Simple D3 clock.” +. https://observablehq.com/@d3/simple-clock.
- Heyen, Frank. 2021+246. “BarChart Clock.” +. https://observablehq.com/@fheyen/barchart-clock.
- Johnson, Ian. 2021+090. “Draggable World Map Coordinates Input.” +. https://observablehq.com/@enjalot/draggable-world-map-coordinates-input.
- Bridges, Dan. 2021+311. “Visualizing Seasonal Daylight.” +. https://observablehq.com/@dbridges/visualizing-seasonal-daylight.
- Clements, John. 2014+091, “Decimal Time Zones.” +. https://www.brinckerhoff.org/blog/2014/05/31/decimal-time-zones.
function unix2dote(unix, zone, offset = 719468) {
return [(unix ?? Date.now()) / 86400000 + (
zone = zone ?? -Math.round(
(new Date).getTimezoneOffset() / 144)
) / 10 + offset, zone]
}
function unix2dote1(unix, zone, offset = 719468) {
return [
(unix ?? Date.now()) / 86400000
+ (zone = zone ?? (10 - Math.round(
(new Date).getTimezoneOffset() / 144)) % 10
) / 10 + offset, zone]
}
function dote2date(dote, zone = 0) {
const cote = Math.floor((
dote >= 0 ? dote
: dote - 146096
) / 146097),
dotc = dote - cote * 146097,
yotc = Math.floor((dotc
- Math.floor(dotc / 1460)
+ Math.floor(dotc / 36524)
- Math.floor(dotc / 146096)
) / 365);
return [
yotc + cote * 400,
dotc - (yotc * 365
+ Math.floor(yotc / 4)
- Math.floor(yotc / 100)
), zone]}
function getEot(day) {
const gamma = 2 * Math.PI * day / 365
return (
0.0011114386869002235 +
-4.155124400404918 * Math.cos(gamma) +
-2.9710873225303756 * Math.sin(gamma) +
-4.635570414590801 * Math.cos(2 * gamma) +
5.116176940364264 * Math.sin(2 * gamma)
)
}
function getSda(day) {
const gamma = 2 * Math.PI * day / 365
return (
0.001091553079960761 +
-0.023117708562197206 * Math.cos(gamma) +
0.060320423699681706 * Math.sin(gamma) +
0.0006152631125526081 * Math.cos(2 * gamma) +
0.0008981745798778195 * Math.sin(2 * gamma)
)
}
function getSha(latitude, solarDeclinationAngle, solarZenithAngle = 0.252314) {
const ratio = (
costau(.252314) - sintau(latitude) * sintau(solarDeclinationAngle)) / (
costau(latitude) * costau(solarDeclinationAngle))
return ratio >= 1 ? 0 : ratio <= -1 ? Math.PI : Math.acos(ratio)
}
cospi = require( 'https://cdn.jsdelivr.net/gh/stdlib-js/math-base-special-cospi@umd/browser.js' )
sinpi = require( 'https://cdn.jsdelivr.net/gh/stdlib-js/math-base-special-sinpi@umd/browser.js' )
function sintau(turn) {
return sinpi(2 * turn);
}
function costau(turn) {
return cospi(2 * turn);
}
shaRadiMapPlot = getSha(latTurnMapPlot, getSda(selYda))
shaHalfMapPlot = shaRadiMapPlot / Math.PI
shaTurnMapPlot = shaHalfMapPlot / 2
shaDegrMapPlot = shaHalfMapPlot * 180
eotTurnMapPlot = getEot(selYda) / 1000
lonTurnMapPlot = long2turn(location[0], 0)
latTurnMapPlot = lati2turn(location[1], 0)
astDecMapPlot = (0.95 + zeroTime + lonTurnMapPlot + eotTurnMapPlot) % 1 * 10
astDegMapPlot = astDecMapPlot * 36
shaDoyInput = getSha(latInput / 1000, getSda(doyInput))
daytimeDuration = shaDoyInput / Math.PI
noonDiff = daytimeDuration / 2
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", "3.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 / 2.8) * 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 : width < 800 ? 1.4 : width < 900 ? 1.5 : width < 1000 ? 1.7 : width < 1100 ? 1.8 : width < 1200 ? 1.9 : 2) * 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 < 450 ? .95 : width < 500 ? 1 : width < 600 ? 1.2 : width < 700 ? 1.3 : width < 800 ? 1.4 : width < 900 ? 1.5 : width < 1000 ? 1.7 : width < 1100 ? 1.8 : width < 1200 ? 1.9 : 2) * fontSize))
.call((g) => g.selectAll(".tick text").attr("color", "black"))
.call((g) => g.selectAll(".tick line").attr("stroke-dasharray", "5 3"));
root
.append("text")
.text("Time of day")
.attr("x", -chartHeight / 5 + (width < 275 ? -2 : width < 300 ? -6 : width < 325 ? -8 : width < 350 ? -15 : width < 375 ? -20 : width < 400 ? -28 : width < 425 ? -30 : width < 450 ? -32 : width < 475 ? -36 : width < 500 ? -41 : width < 525 ? -42 : width < 550 ? -48 : width < 575 ? -49 : width < 600 ? -4 : width < 650 ? -7 : width < 675 ? -12 : width < 700 ? -16 : width < 725 ? -15 : width < 750 ? -20 : width < 775 ? -5 : width < 875 ? -3 : width < 975 ? 13 : width < 1025 ? 36 : width < 1075 ? 28 : width < 1125 ? 28 : width < 1175 ? 28 : width < 1275 ? 35 : 32))
.attr("y", margin.left - (width < 275 ? 12 : width < 300 ? 16 : width < 325 ? 17 : width < 350 ? 18 : width < 375 ? 19 : width < 400 ? 20 : width < 425 ? 21 : width < 450 ? 22 : width < 475 ? 23 : width < 500 ? 24 : width < 525 ? 25 : width < 550 ? 26 : width < 575 ? 27 : width < 600 ? 28 : width < 650 ? 29 : width < 675 ? 30 : width < 700 ? 31 : width < 725 ? 40 : width < 750 ? 49 : width < 775 ? 46 : width < 875 ? 47 : width < 925 ? 52 : width < 975 ? 53 : width < 1025 ? 54 : width < 1075 ? 55 : width < 1175 ? 64 : width < 1275 ? 68 : 64))
.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 ? 45 : width < 700 ? 39 : width < 750 ? 35 : width < 850 ? 45 : width < 950 ? 55 : width < 1050 ? 65 : width < 1150 ? 75 : width < 1250 ? 85 : width < 1350 ? 95 : 109))
.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", chartWidth / 20 + (width < 275 ? 1 : width < 300 ? 2 : width < 325 ? 3 : width < 350 ? 4 : width < 375 ? 5 : width < 400 ? 6 : width < 450 ? 7 : width < 475 ? 8 : width < 500 ? 9 : width < 550 ? 10 : width < 600 ? 11 : width < 650 ? 12 : width < 700 ? 13 : width < 750 ? 14 : width < 850 ? 5 : width < 950 ? -6 : width < 1050 ? -8 : width < 1150 ? -8 : width < 1250 ? -8 : width < 1350 ? -8 : width < 1400 ? -8 : -100))
.attr("y", chartHeight / 4 + (width < 300 ? 81 : width < 400 ? 82 : width < 450 ? 83 : width < 500 ? 52 : width < 600 ? 54 : width < 700 ? 85 : width < 800 ? 94 : width < 900 ? 99 : width < 1000 ? 109 : width < 1100 ? 139 : width < 1200 ? 179 : 154))
.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", chartWidth / 20 + (width < 275 ? 1 : width < 300 ? 2 : width < 325 ? 3 : width < 350 ? 4 : width < 375 ? 5 : width < 400 ? 6 : width < 450 ? 7 : width < 475 ? 8 : width < 500 ? 9 : width < 550 ? 10 : width < 600 ? 11 : width < 650 ? 12 : width < 700 ? 13 : width < 750 ? 14 : width < 850 ? 19 : width < 950 ? 29 : width < 1050 ? 29 : width < 1150 ? 29 : width < 1250 ? 29 : width < 1350 ? 29 : width < 1400 ? 33 : -62))
.attr("y", chartHeight / 4 + (width < 300 ? 72 : width < 400 ? 73 : width < 450 ? 74 : width < 500 ? 68 : width < 600 ? 70 : width < 700 ? 74 : width < 800 ? 72 : width < 900 ? 122 : width < 1000 ? 132 : width < 1100 ? 142 : width < 1200 ? 152 : 182))
.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", chartWidth / 20 + (width < 275 ? -59 : width < 300 ? -58 : width < 325 ? -57 : width < 350 ? -56 : width < 375 ? -55 : width < 400 ? -54 : width < 450 ? -53 : width < 475 ? -52 : width < 500 ? -51 : width < 550 ? -50 : width < 600 ? -51 : width < 650 ? -52 : width < 700 ? -53 : width < 750 ? -74 : width < 850 ? -132 : width < 950 ? -132 : width < 1050 ? -132 : width < 1150 ? -132 : width < 1250 ? -132 : width < 1350 ? -132 : width < 1400 ? -148 : -240))
.attr("y", chartHeight / 4 + (width < 300 ? 81 : width < 400 ? 82 : width < 450 ? 83 : width < 500 ? 52 : width < 600 ? 54 : width < 700 ? 85 : width < 800 ? 94 : width < 900 ? 99 : width < 1000 ? 109 : width < 1100 ? 139 : width < 1200 ? 179 : 154))
.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", chartWidth / 20 + (width < 275 ? 9 : width < 300 ? 8 : width < 325 ? 7 : width < 350 ? 6 : width < 375 ? 5 : width < 400 ? 4 : width < 450 ? 3 : width < 475 ? 2 : width < 500 ? 1 : width < 550 ? 0 : width < 600 ? -1 : width < 650 ? -2 : width < 700 ? -3 : width < 750 ? -19 : width < 850 ? -98 : width < 950 ? -98 : width < 1050 ? -98 : width < 1150 ? -98 : width < 1250 ? -98 : width < 1350 ? -98 : width < 1400 ? -108 : -201))
.attr("y", chartHeight / 4 + (width < 300 ? 72 : width < 400 ? 73 : width < 450 ? 74 : width < 500 ? 68 : width < 600 ? 70 : width < 700 ? 74 : width < 800 ? 72 : width < 900 ? 122 : width < 1000 ? 132 : width < 1100 ? 142 : width < 1200 ? 152 : 182))
.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/18);
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 > 1000 ? width / 50 : width / 40}px monospace; position: relative; top: ${-28 - 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
selYda = 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())
selYdaDay = textcolor(selYda.toFixed(3).padStart(6, "0"), d3.color(piecewiseColor(selYda / 365)).formatHex())
selYdaDec = textcolor((selYda * 10).toFixed(4).padStart(9, "0"), d3.color(piecewiseColor(selYda / 365)).formatHex())
decZoneHsl = textcolor(selectedZone, d3.color(piecewiseColor(selectedZone / 10)).formatHex())
viewof selectedDate = Inputs.input(new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().getHours()))
viewof selectedHour = Inputs.input(new Date(2022, new Date().getMonth(), new Date().getDate(), new Date().getHours()).getHours())
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input", {bubbles: true}));
}
hD121 = d3.hsl(piecewiseColor(121 / 365)).h
hD268 = d3.hsl(piecewiseColor(268 / 365)).h
hD305 = d3.hsl(piecewiseColor(305 / 365)).h
hD306 = d3.hsl(piecewiseColor(306 / 365)).h
h1by320 = d3.hsl(piecewiseColor(1 / 320)).h
h1by8640 = d3.hsl(piecewiseColor(1 / 8640)).h
hues = Object.fromEntries([
.0083,
.0166,
.0229,
.025,
.287,
.0333,
.0416,
.05,
.125,
.333,
.375,
.429,
.533,
.969,
.999,
].map(i => [i, d3.hsl(piecewiseColor(i)).h])
);
// https://observablehq.com/@mattdzugan/population-by-time-zone-creating-a-dataset
populationByTimeZone = FileAttachment("../../asset/populationByTimeZone.json").json();
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)
utcOffsetM = -(new Date).getTimezoneOffset()
utcOffsetD = utcOffsetM / 144
utcOffsetP = (utcOffsetD + 10) % 10
decZone = ydz[2]
decZonePos = (decZone + 10) % 10
utcOffDiff = parseFloat((Math.round(utcOffsetD) - utcOffsetD).toFixed(2))
utcOffHslM = textcolor(utcOffsetM, `hsl(${d3.hsl(piecewiseColor(utcOffsetP / 10)).h}` + slStr)
utcOffHslD = textcolor(parseFloat(utcOffsetD.toFixed(2)), `hsl(${d3.hsl(piecewiseColor(utcOffsetP / 10)).h}` + slStr)
decZonHslP = textcolor(decZonePos, `hsl(${d3.hsl(piecewiseColor(decZonePos / 10)).h}` + slStr)
utcOffsetMdiffHsl = textcolor(parseFloat((utcOffDiff * 144).toFixed(2)), `hsl(${d3.hsl(piecewiseColor(utcOffDiff / 10)).h}` + slStr)
utcOffDiffHsl = textcolor(utcOffDiff, `hsl(${d3.hsl(piecewiseColor(utcOffDiff / 10)).h}` + slStr)
function date2dote(year = 1969, doty = 306, zone = 0) {
const cote = Math.floor((year >= 0 ? year : year - 399) / 400), yote = year - cote * 400;
return [cote * 146097 + yote * 365 + Math.floor(yote / 4) - Math.floor(yote / 100) + doty, zone]
}
leapSecondDotes = [
[1972, 121],
[1972, 305],
[1973, 305],
[1974, 305],
[1975, 305],
[1976, 305],
[1977, 305],
[1978, 305],
[1979, 305],
[1981, 121],
[1982, 121],
[1983, 121],
[1985, 121],
[1987, 305],
[1989, 305],
[1990, 305],
[1992, 121],
[1993, 121],
[1994, 121],
[1995, 305],
[1997, 121],
[1998, 305],
[2005, 305],
[2008, 305],
[2012, 121],
[2015, 121],
[2016, 305],
].map(x => date2dote(...x)[0])
leapSecondDote = date2dote(leapSecondYear, leapSecondDate)[0]
leapCount = leapSecondDotes.filter(dote => leapSecondDote > dote).length;
leapColor = d3.color(piecewiseColor(leapCount / 86400)).formatHex()
leapCountHsl = textcolor(leapCount, leapColor)
frac = require("fraction.js")
leapFrac = frac(leapCount).div(8640).toFraction()
leapTzoHsl0 = textcolor(leapFrac, leapColor)
leapTzoHsl1 = textcolor(leapFrac, leapColor)
leapTzoHsl2 = textcolor(leapFrac, leapColor)
hmsTzo = frac(hmsinput[0]).div(24).add(hmsinput[1], 1440).add(hmsinput[2], 86400).mod(1)
hmsTzox10 = hmsTzo.mul(10)
pluralx10 = hmsTzox10.equals(1) ? "" : "s"
hmsRounded = hmsTzox10.round(digits - 1)
hmsRef = hmsRounded.sub(hmsTzox10)
hmsRefHsl = textcolor(hmsRef.abs().toFraction(), d3.color(piecewiseColor(Number(hmsRef.div(10)))).formatHex())
hmsRefSign = hmsRef.lt(0) ? "-" : "+"
hmsTzoHsl = textcolor(hmsTzox10.toFraction(), d3.color(piecewiseColor(Number(hmsTzo))).formatHex())
hmsRodHsl = textcolor(hmsRounded, d3.color(piecewiseColor(Number(hmsRounded.div(10)))).formatHex())
hmsTod = (zeroTime + hmsTzo) % 1
hmsTodHsl = textcolor((hmsTod * 10).toFixed(4), d3.color(piecewiseColor(hmsTod)).formatHex())
leapTod = (zeroTime + leapCount / 86400) % 1
leapDecidayTod = (leapTod * 10).toFixed(4)
leapTodHsl0 = textcolor(leapDecidayTod, d3.color(piecewiseColor(leapTod)).formatHex())
leapTodHsl1 = textcolor(leapDecidayTod, d3.color(piecewiseColor(leapTod)).formatHex())
longInputHsl = textcolor(lonInput, d3.color(piecewiseColor(lonInput / 10)).formatHex())
eot = getEot(leapSecondDate)
astTzo = eot + lonInput
astTzoHsl = textcolor(astTzo.toFixed(4), d3.color(piecewiseColor(astTzo / 10)).formatHex())
eotHsl = textcolor(eot.toFixed(4), d3.color(piecewiseColor(eot / 10)).formatHex())
eotSum = (zeroTime + astTzo + 10) % 10
eotSumHsl = textcolor(eotSum.toFixed(4), d3.color(piecewiseColor(eotSum / 10)).formatHex())
// https://observablehq.com/@mcmcclur/adaptive-plotter
function build_samples(f, a, b, opts = {}) {
let { N = 9, max_depth = 6 } = opts;
let dx = (b - a) / N;
let root_intervals = Array.from({ length: N }).map(
(_, i) => new Interval(a + i * dx, a + (i + 1) * dx, 0)
);
root_intervals.forEach((I) => {
I.fa = f(I.a);
I.fb = f(I.b);
});
root_intervals.reverse();
let stack = root_intervals;
let cnt = 0;
let pts = [];
let nodeRight, nodeLeft;
while (stack.length > 0 && cnt++ < 100000) {
let node = stack.pop();
if (test(f, node, opts)) {
let midpoint = node.midpoint;
let new_depth = node.depth + 1;
if (new_depth <= max_depth) {
let a_left = node.a;
let b_left = midpoint;
nodeLeft = new Interval(a_left, b_left, new_depth);
nodeLeft.fa = f(a_left);
nodeLeft.fb = f(b_left);
node.left = nodeLeft;
let a_right = midpoint;
let b_right = node.b;
nodeRight = new Interval(a_right, b_right, new_depth);
nodeRight.fa = f(a_left);
nodeRight.fb = f(b_left);
node.right = nodeRight;
stack.push(nodeRight);
stack.push(nodeLeft);
} else {
pts.push(node.a);
}
} else {
pts.push(node.a);
}
}
pts.push(b);
// pts = pts.map(x => ({ x: x, y: f(x) }));
pts = pts.map((x) => [x, f(x)]);
if (opts.show_roots) {
let function_roots = [];
pts.forEach(function (o, i) {
if (i < pts.length - 1 && Math.sign(o.y) != Math.sign(pts[i + 1].y)) {
function_roots.push((o.x + pts[i + 1].x) / 2);
}
});
pts.function_roots = function_roots;
}
return pts;
}
function test(f, I, opts = {}) {
let { angle_tolerance = 0.01, check_roots = false } = opts;
let a = I.a;
let b = I.b;
let dx2 = (b - a) / 2;
let m = (a + b) / 2;
let fm = f(m);
I.midpoint = m;
I.f_mid = fm;
if (check_roots && Math.sign(I.fa) != Math.sign(I.fb)) {
return true;
}
let alpha = Math.atan((I.f_mid - I.fa) / dx2);
let beta = Math.atan((I.fb - I.f_mid) / dx2);
return Math.abs(alpha - beta) > angle_tolerance;
}
class Interval {
constructor(a, b, depth) {
this.a = a;
this.b = b;
this.depth = depth;
}
}
colors = ({
night: "#719fb6",
day: "#ffe438",
grid: "#4b6a79",
ocean: "lightblue",
land: "#f5f1dc",
sun: "#ffe438"
})
// https://observablehq.com/@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();
}
// https://observablehq.com/@d3/simple-clock
// https://observablehq.com/@drio/lets-build-an-analog-clock
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", "midclock");
svg
.append("text")
.text(`-${decTimeN}-${selectedZone}`)
.attr("x", clockRadius + margin)
.attr("y", clockRadius * 2 + margin * 2.1)
.attr("text-anchor", "middle")
.attr("font-size", 32)
.attr("font-family", "monospace");
drawClock();
// Animation
const interval = setInterval(
() => {
updateData();
moveHands();
},
!fancySecondsOFF ? 10 : 864
);
invalidation.then(() => clearInterval(interval));
return svg.node();
}
sunclock = {
const radius = 200;
const width = radius * 2;
const height = radius * 2;
//let _minutes = 0;
const svg = d3
.create("svg")
.attr("id", "btmclock")
.attr("viewBox", [0, 0, width, height])
.style("margin-top", "1px")
.style("max-width", "200px");
const face = svg
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
for (const angle of angles) {
const arc = d3
.arc()
.innerRadius(0)
.outerRadius(radius)
.startAngle(angle.start)
.endAngle(angle.end);
face.append("path").attr("d", arc).attr("fill", angle.color);
}
const sun = face
.append("circle")
.attr("r", 15)
.attr("cx", 125 * -sin(astDegMapPlot))
.attr("cy", 125 * cos(astDegMapPlot))
.style("fill", "white");
const ringContainer = svg
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
const ring = ringContainer
.append("circle")
.attr("r", 125)
.style("opacity", 0.2)
.style("fill", "transparent")
.style("stroke", "white")
.style("stroke-width", 3);
const events = [
data.astronomicalSunrise,
data.astronomicalSunset,
data.nauticalSunrise,
data.nauticalSunset,
data.civilSunrise,
data.civilSunset,
data.sunrise,
data.sunset,
data.solarNoon
];
for (const e of events) {
ringContainer
.append("circle")
.attr("r", 5)
.attr("cx", 125 * Math.sin(e))
.attr("cy", 125 * -Math.cos(e))
.style("opacity", 0.4)
.style("fill", "white");
}
const digitalClock = svg
.append("g")
.attr(
"transform",
`translate(${width / 2 + 45 * sin(astDegMapPlot)}, ${
height / 2 + 45 * -cos(astDegMapPlot)
})`
);
const digitalClockBase = digitalClock
.append("circle")
.attr("r", 100)
.style("opacity", 0.21)
.style("fill", "white");
// const digitalClockTicks = digitalClock
// .selectAll(".digital-clock-tick")
// .data(d3.range(0, 60))
// .enter()
// .append("line")
// .attr("class", "digital-clock-tick")
// .attr("x1", 0)
// .attr("x2", 0)
// .attr("y1", 100 - 5)
// .attr("y2", 100 - 10)
// .attr("stroke", (d) => (d > seconds ? "black" : "white"))
// .attr("stroke-width", "3")
// .attr("stroke-linecap", "round")
// .style("mix-blend-mode", "luminance")
// .style("opacity", 0.6)
// .attr("transform", (d) => `rotate(${180 + d * 6})`);
const clockText = digitalClock
.append("text")
.attr("y", 25)
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("font-family", "Helvetica, Arial, sans-serif")
.style("opacity", 1)
.attr("font-weight", 600)
.attr("font-size", "75px")
.text(astDecMapPlot.toFixed(2));
const ticks = face
.selectAll(".tick")
.data(d3.range(0, 20))
.enter()
.append("line")
.attr("class", "tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", radius - 5)
.attr("y2", radius - 20)
.attr("stroke", "white")
.attr("stroke-width", "3")
.attr("stroke-linecap", "round")
.style("mix-blend-mode", "screen")
.style("opacity", 1)
.attr("transform", (d) => `rotate(${d * 18})`);
const minuteTicks = face
.selectAll(".tick")
.data(d3.range(0, 200))
.enter()
.append("line")
.attr("class", "tick")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", radius - 5)
.attr("y2", radius - 15)
.attr("stroke", "white")
.attr("stroke-width", "2")
.attr("stroke-linecap", "round")
.style("mix-blend-mode", "screen")
.style("opacity", 0.6)
.attr("transform", (d) => `rotate(${d * 2})`);
const hourLabels = face
.selectAll(".hour-label")
.data(d3.range(0, 10))
.enter()
.append("text")
.attr("class", "hour-label")
.attr("x", (d) => 160 * -Math.sin(toRadians((d / 10) * 360)))
.attr("y", (d) => 160 * Math.cos(toRadians((d / 10) * 360)) + 12)
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("font-family", "Helvetica, Arial, sans-serif")
.attr("font-weight", 600)
.attr("font-size", "38px")
.style("opacity", 0.7)
.text((d) => (String(d)));
return svg.node();
}
minutesAndSeconds = {
while (true) {
await Promises.delay(1000);
const date = new Date();
yield {
minutes: date.getHours() * 60 + date.getMinutes(),
seconds: date.getSeconds()
};
}
}
function minutesTo12HourTime(minutes) {
const decidays = minutes / 144 % 10
return decidays.toFixed(2);
}
viewof date = Inputs.date({ label: "Date", value: Date.now() })
JD = getJulianDate(date)
T = (JD - 2451545.0) / 36525
sun_geometric_mean_longitude = 280.46646 + 36000.76983 * T + 0.0003032 * T ** 2
sun_mean_anomaly = 357.52911 + 35999.05029 * T - 0.0001537 * T ** 2
earth_eccentricity_orbit = 0.016708634 - 0.000042037 * T - 0.0000001267 * T ** 2
sun_equation_of_the_center = (1.914602 - 0.004817 * T - 0.000014 * T ** 2) * sin(sun_mean_anomaly) + (0.019993 - 0.000101 * T) * sin(2 * sun_mean_anomaly) + 0.000289 * sin(3 * sun_mean_anomaly)
sun_true_longitude = sun_geometric_mean_longitude + sun_equation_of_the_center
sun_true_anomaly = sun_mean_anomaly + sun_equation_of_the_center
omega = 125.04 - 1934.136 * T
sun_aparrent_longitude = sun_true_longitude - 0.00569 - 0.00478 * sin(omega)
sun_ecliptic_obliquity = 23 + (26 + (21.448 - 46.815 * T - 0.00059 * T ** 2 + 0.001813 * T ** 3) / 60) / 60
sun_ecliptic_obliquity_corrected = sun_ecliptic_obliquity + 0.00256 * cos(omega)
sun_right_ascension = Math.atan2(
cos(sun_ecliptic_obliquity_corrected) * sin(sun_aparrent_longitude),
cos(sun_aparrent_longitude)
)
sun_declination = Math.asin(
sin(sun_ecliptic_obliquity_corrected) * sin(sun_aparrent_longitude)
)
y = Math.tan(toRadians(sun_ecliptic_obliquity_corrected) / 2) ** 2
equation_of_time = toDegrees(
y * sin(2 * (sun_geometric_mean_longitude)) -
2 * earth_eccentricity_orbit * sin((sun_mean_anomaly)) +
4 * earth_eccentricity_orbit * y * sin((sun_mean_anomaly)) * cos(2 * (sun_geometric_mean_longitude)) -
(1 / 2) * y ** 2 * sin(4 * (sun_geometric_mean_longitude)) -
(5 / 4) * earth_eccentricity_orbit ** 2 * sin(2 * (sun_mean_anomaly))
) * 4
hour_angle = shaDegrMapPlot
sun_transit = solarNoonMinu
sun_rise = sun_transit - hour_angle * 4
sun_set = sun_transit + hour_angle * 4
sun_duration = sun_set - sun_rise
sun_civil_rise = sun_transit - (hour_angle + 6) * 4
sun_nautical_rise = sun_transit - (hour_angle + 12) * 4
sun_astronomical_rise = sun_transit - (hour_angle + 18) * 4
sun_civil_set = sun_transit + (hour_angle + 6) * 4
sun_nautical_set = sun_transit + (hour_angle + 12) * 4
sun_astronomical_set = sun_transit + (hour_angle + 18) * 4
data = ({
astronomicalSunrise:
toRadians((sun_astronomical_rise / 1440) * 360) + Math.PI,
nauticalSunrise: toRadians((sun_nautical_rise / 1440) * 360) + Math.PI,
civilSunrise: toRadians((sun_civil_rise / 1440) * 360) + Math.PI,
sunrise: toRadians((sun_rise / 1440) * 360) + Math.PI,
sunset: toRadians((sun_set / 1440) * 360) + Math.PI,
civilSunset: toRadians((sun_civil_set / 1440) * 360) + Math.PI,
nauticalSunset: toRadians((sun_nautical_set / 1440) * 360) + Math.PI,
astronomicalSunset: toRadians((sun_astronomical_set / 1440) * 360) + Math.PI,
solarNoon: toRadians((sun_transit / 1440) * 360) + Math.PI,
dayLength: sun_duration
})
angles = [
{
start: data.astronomicalSunrise,
end: data.nauticalSunrise,
color: "rgb(17,49,86)"
},
{
start: data.nauticalSunrise,
end: data.civilSunrise,
color: "rgb(23,61,112)"
},
{ start: data.civilSunrise, end: data.sunrise, color: "rgb(35,85,155)" },
{ start: data.sunrise, end: data.sunset, color: "rgb(70,155,245)" },
{ start: data.sunset, end: data.civilSunset, color: "rgb(35,85,155)" },
{
start: data.civilSunset,
end: data.nauticalSunset,
color: "rgb(23,61,112)"
},
{
start: data.nauticalSunset,
end: data.astronomicalSunset,
color: "rgb(17,49,86)"
},
{
start: data.astronomicalSunset,
end: data.astronomicalSunrise + Math.PI * 2,
color: "rgb(10,34,58)"
}
]
getJulianDate = (timestamp) => {
return new Date(timestamp) / 86400000 + 2440587.5;
}
sin = (x) => Math.sin(toRadians(x))
cos = (x) => Math.cos(toRadians(x))
solarNoonTurn = (9.55 + selectedZone / 10 - lonTurnMapPlot - eotTurnMapPlot) % 1
solarNoonMinu = solarNoonTurn * 1440
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;
}
.clocks {
margin-top: ${width < 775 ? 400 : width < 780 ? 390 : width < 790 ? 382 : width < 793 ? 374 : width < 800 ? 368 : width < 805 ? 358 : width < 810 ? 350 : width < 813 ? 344 : width < 816 ? 342 : width < 819 ? 334 : width < 822 ? 298 : width < 825 ? 288 : width < 830 ? 258 : width < 835 ? 250 : width < 845 ? 240 : width < 850 ? 232 : width < 855 ? 225 : width < 865 ? 220 : width < 875 ? 180 : width < 900 ? 178 : width < 925 ? 186 : width < 950 ? 190 : width < 975 ? 208 : width < 984 ? 168 : width < 993 ? 150 : width < 1000 ? 258 : width < 1025 ? 252 : width < 1050 ? 240 : width < 1075 ? 230 : width < 1083 ? 228 : width < 1088 ? 218 : width < 1093 ? 178 : width < 1100 ? 146 : width < 1120 ? 130 : width < 1125 ? 112 : width < 1130 ? 110 : width < 1150 ? 112 : width < 1160 ? 113 : width < 1175 ? 126 : width < 1200 ? 134 : width < 1205 ? 140 : width < 1210 ? 138 : width < 1215 ? 144 : width < 1220 ? 142 : width < 1223 ? 134 : width < 1225 ? 114 : width < 1230 ? 110 : width < 1232 ? 108 : width < 1235 ? 86 : width < 1240 ? 84 : width < 1244 ? 82 : width < 1246 ? 80 : width < 1248 ? 78 : width < 1250 ? 56 : width < 1260 ? 54 : width < 1270 ? 58 : width < 1275 ? 62 : width < 1300 ? 68 : width < 1350 ? 78 : width < 1400 ? 82 : 84}px;
}
</style>
`
Reuse
Citation
@online{laptev2024,
author = {Laptev, Martin},
title = {Dec {Time}},
date = {2024},
urldate = {2024},
url = {https://maptv.github.io/dec/time},
langid = {en}
}