html`<style>
.tickLabel, .tickLabel1, .tickLabel2, .timeLabel {
fill: #000;
font-family: sans-serif;
font-size: 20px;
text-anchor: middle;
}
.timeLabel {
text-anchor: start;
}
.timeBar, .timeBarFull {
x: 1px;
height: 25px;
rx: 5px;
stroke: #aaa;
}
.timeBar {
fill: #e8e8e8;
}
.timeBarFull {
fill: #ccffff;
}
.background {
fill: white;
}
.tickDek, .tickDotd, .tickDotd1, .tickC, .tickC1, .tickM, .tickM1, .tickB {
stroke: none;
fill: #666;
width: 1px;
}
</style>`
Observable
tool
observable
quarto
TL;DR
The goal of this blog post, the fourth in my Tools blog post series, is to set up the Visual Studio Code (VSCode) source-code editor to run in the remote development environments provided by the Codespaces and GitPod computing platforms. To accomplish this goal, we will use VSCode to edit a setup script and configuration files in a Git repository (repo) named dotfiles
.
Introduction
If you do not have such a repo, you can create one by following along with the previous post in my tools blog post series, which is shown in Figure 1.
Day-of-the-year
Decalendar
1 is a decimal calendar🗓️system and Declock
2 is a decimal timekeeping⏳system. Instead of months, weeks, hours, minutes, and seconds, Decalendar
and Declock
use a number called the day-of-the-year (doty
)3. Figure 2 displays the current doty
() as a bar chart. The integer part of the doty
is the Decalendar
date4 () and the fractional part is the Declock
time5 ().
The top two bars in Figure 2 (旬 and 日) show the two components of the Decalendar
date (): the dek
6 () and the day-of-the-dek
7 (). A dek
is a group of 10 days that fulfills the role of months and weeks in Decalendar
. Therefore, the dek
and the day-of-the-dek
(dotd
) in Decalendar
dates are analogous to the month and day-of-the-month (dotm
) in calendar dates (mm-dd
) and the week and day-of-the-week (dotw
) in week dates (Www-d
).
The bottom three bars in Figure 2 (%, ‰, and ♫) show components of the Declock
time (): cents
8 (), mils
9 (), and beats
10 (). A cent
is 1% of the day, which is a little less than a quarter hour. A mil
(‰
) is a tenth of a cent
, which is close to a minute and a half. A beat
is 1% of a mil
, which is almost as long as a second, and can be thought of as a heart❤️beat or musical🎵beat with a constant rate or tempo of 69.4̅ beats per minute. In addition to displaying time on clocks🕰️and⌚️watches, beats
can be used to measure durations, such as the time since this webpage was loaded: .
As an alternative to doty
dates, Decalendar
can express dates in fractional years, which are similar to the fractional days used in doty
times. Fractional year dates11 can be combined with years into one number and are useful for marking periods of 73 days in the Decalendar
year. Every 73 days, the Decalendar
fractional year date increases by .2 (⅕). The current Decalendar
fractional year date is .
Now it’s your turn! Move the sliders🎚️in Example 1 to adjust the doty
, fractional year, month, and dotm
values and see the corresponding 1) Northern Hemisphere season, 2) Southern Hemisphere season, and 3) Zodiac sign in ../../List 1. Try selecting a special date, like your birthday🎂! You can also press the Play▶️button and then sit back and watch the sliders cycle🔄back and forth from the start to the end of the Decalendar
year. The sliders cycle through an entire year in 36.5 beats
, a million times faster🏎️than the actual speed of time!
Example 1
Year
Doty
dates (day
) and timestamps (day.stamp
) do not include years and thus can be reused from year to year. When a doty
is combined with a year, it forms a Decalendar
ordinal
12 (deco
) and represents a specific date (year+day
) or time (year+day.stamp
) in a specific year, instead of a date (day
) or time (day.stamp
) that occurs every year or every leap year. Using the Observable datetime input in Example 2, you can select a year, month, dotm
, hour, and minute to see the equivalent 1) deco
timestamp (year+day.clock
) and 2) year date (year.yyy
) in ../../List 2. For comparison, the current deco
timestamp is .
Example 2
In Example 3, you can enter numeric year and doty
values or type in a free-form deco
to see the resulting 1) ISO 8601 timestamp (year-mm-ddThh:mm:ss
) and 2) year date in ../../List 3. Example 3 also has a Play▶️button in that cycles from Day
0
to Day
365
of Year
1969
. The cycle goes up to Day
0
of Year
1970
, because Year
1969
is a common year and does not have a Day
365
.
Interacting with the number inputs in Example 3, standardizes the deco
in the text input to ensure years are integers and doty
values are positive numbers below 366. Nevertheless, all of the inputs in Example 3 support negative and fractional year and doty
values. Negative doty
values shift dates backward in time from the start of a given year into a previous year. Similarly, negative years shift dates backward starting from Year
0
, which is 1 BCE (Before the Common Era).
Example 3
Dek
As mentioned in Section 0.3, deks
function as both months and weeks in Decalendar
. A dek
consists of 2 groups of 5 days called pents
13. Each pent
can follow a sequence of workdays and restdays called a pently
schedule14. fig-schedules compares the typical weekly schedule and the Schedule
3
pently
schedule. Like other pently
schedules, Schedule
3
is named after the number of workdays it contains. The 3 workdays in Schedule
3
are followed by a 2-day pentend
, the Decalender
equivalent of a weekend.
Unlike Schedule
3
and the other pently
schedules, the weekly schedule is asymmetric and divides up workdays into proportions that are easier to express as fractions: 3/8 (.375), 1/3 (.3̅), and 7/24 (0.2916̅). In contrast, pently
schedules divides each workday into simple, symmetrical proportions that are never repeating decimal numbers. The Schedule
3
workday is longer the typical 9-to-5, but the longer workday is compensated by more frequent restdays.
Example 4 provides the opportunity to explore all four of the pently
schedules. Schedule
3
is the default pently
schedule, and should be followed unless there is a compelling reason to do otherwise. Despite having different numbers of workdays, all pently
schedules keep the amount of spent at work constant at 1.2 days (120 cents
) per pent
. Every pently
schedule also splits up workdays symmetrically so that the time before work is equal to the time after work. You can select which schedule to view in Example 4 using the Observable radio inputs.
You can also use the interval sliders to create a custom schedule and the Download⬇️button to obtain the schedule data. Each interval is summarized by a Declock
slice
shown next to its slider. Decalendar
and Declock
slices
are inspired by array slicing in computer programming and are used to represent date and time intervals. The slice
that summarizes the Schedule
3
workday is .3:.7
, where .3 (7.2 hours) is the start
and .7 (16.8 hours) is the stop
.
Example 4
Slices
can be used to select Declock
time intervals, as in Example 4 above, or Decalendar
date intervals, as in Example 5 below. Decalendar
date intervals can represent events take place over multiple days. In addition to slices
, Decalendar
and Declock
intervals can also be chosen using spreads
. Use the Observable button and checkbox inputs in Example 5 to display different dotd
combinations in Figure 4. Figure 4 shows dates selected by a slice
in blue, dates selected by a spread
in orange, dates selected by both in green, and dates selected by neither in gray.
The Example 5 button and checkbox inputs will modify the associated slice
and spread
text and numeric inputs as needed to select the desired days-of-the-dek
. The numeric inputs for slices
are the start
🎬index, the stop
🛑index, and the step
👣size, which are separated by colons (start:stop:step
) when combined into a slice
. Similarly, the numeric inputs for spreads
are the start
🎬or🛑stop
index, the span
🪽size, the split
size, and the space
size, which are typically delimited by greater-than signs (start>span>split>space
) or a mix of greater-than and less-than signs (stop<span>split>space
).
The numeric inputs in Example 5 cannot capture the full power of spreads
and slices
, because slices
can have any number of steps
and, likewise, spreads
can have any number of alternating split
and space
sizes. We can select all even or odd days-of-the-dek
using a slice
with a single step
(::2
or 1::2
) or a spread
with one split
-space
pair (>>>1
or 1>>>1
). More complex patterns, such as prime or composite days-of-the-dek
, require multiple step
sizes for slices
(2:::2:2:5
or 4::2:2::5
) or multiple split
-space
pairs (2>>2>1>>1>>4
or 4>>>1>>1>2>4
).
Example 5
References
Appendix
Doty
Observable code
Functions
function set(input, value) {
.value = value;
input.dispatchEvent(new Event("input", {bubbles: true}));
input
}// https://observablehq.com/@mbostock/scrubber
function Scrubber(values, {
= value => value,
format = 0,
initial = 1,
direction = null,
delay = true,
autoplay = true,
loop = null,
loopDelay = false
alternate = {}) {
} = Array.from(values);
values const form = html`<form style="font: 12px var(--sans-serif); font-variant-numeric: tabular-nums; display: flex; height: 33px; align-items: center;">
<button name=b type=button style="background-color:#002ead;color:#fff;border-radius:10px;margin-left:.4em;width: 5em;"></button>
<label style="display: flex; align-items: center;">
<input name=i type=range min=0 max=${values.length - 1} value=${initial} step=1 style="display: none;">
<output name=o style="display: none;"></output>
</label>
</form>`;
let frame = null;
let timer = null;
let interval = null;
function start() {
.b.textContent = "Pause";
formif (delay === null) frame = requestAnimationFrame(tick);
else interval = setInterval(tick, delay);
}function stop() {
.b.textContent = "Play";
formif (frame !== null) cancelAnimationFrame(frame), frame = null;
if (timer !== null) clearTimeout(timer), timer = null;
if (interval !== null) clearInterval(interval), interval = null;
}function running() {
return frame !== null || timer !== null || interval !== null;
}function tick() {
if (form.i.valueAsNumber === (direction > 0 ? values.length - 1 : direction < 0 ? 0 : NaN)) {
if (!loop) return stop();
if (alternate) direction = -direction;
if (loopDelay !== null) {
if (frame !== null) cancelAnimationFrame(frame), frame = null;
if (interval !== null) clearInterval(interval), interval = null;
= setTimeout(() => (step(), start()), loopDelay);
timer return;
}
}if (delay === null) frame = requestAnimationFrame(tick);
step();
}function step() {
.i.valueAsNumber = (form.i.valueAsNumber + direction + values.length) % values.length;
form.i.dispatchEvent(new CustomEvent("input", {bubbles: true}));
form
}.i.oninput = event => {
formif (event && event.isTrusted && running()) stop();
.value = values[form.i.valueAsNumber];
form.o.value = format(form.value, form.i.valueAsNumber, values);
form;
}.b.onclick = () => {
formif (running()) return stop();
= alternate && form.i.valueAsNumber === values.length - 1 ? -1 : 1;
direction .i.valueAsNumber = (form.i.valueAsNumber + direction) % values.length;
form.i.dispatchEvent(new CustomEvent("input", {bubbles: true}));
formstart();
;
}.i.oninput();
formif (autoplay) start();
else stop();
.disposal(form).then(stop);
Inputsreturn form;
}// https://observablehq.com/@juang1744/transform-input/1
= function(target, {bind: source, transform = identity, involutory = false, invert = involutory ? transform : inverse(transform)} = {}){
transformInput if (source === undefined) {
= target;
source = html`<div>${source}</div>`;
target
}function sourceInputHandler() {
.removeEventListener("input", targetInputHandler);
targetsetTransform(target).to(transform(source.value)).andDispatchEvent();
.addEventListener("input", targetInputHandler);
target
}function targetInputHandler() {
.removeEventListener("input", sourceInputHandler);
sourcesetTransform(source).to(invert(target.value)).andDispatchEvent();
.addEventListener("input", sourceInputHandler);
source
}.addEventListener("input", sourceInputHandler);
source.addEventListener("input", targetInputHandler);
target.then(() => {
invalidation.removeEventListener("input", sourceInputHandler);
source.removeEventListener("input", targetInputHandler);
target;
})
sourceInputHandler();
return target;
}= (input) => ({to: (value) => (input.value = value, {andDispatchEvent: (event = new Event("input")) => input.dispatchEvent(event)})});
setTransform function inverse(f) {
switch (f) {
case identity: return identity;
case Math.sqrt: return square;
case Math.log: return Math.exp;
case Math.exp: return Math.log;
default: return (x => solve(f, x, x));
}function solve(f, y, x = 0) {
const dx = 1e-6;
let steps = 100, deltax, fx, dfx;
do {
= f(x)
fx = (f(x + dx) - fx) || dx;
dfx = dx * (fx - y)/dfx
deltax -= deltax;
x while (Math.abs(deltax) > dx && --steps > 0);
} return steps === 0 ? NaN : x;
}
function square(x) {
return x * x;
}
}function identity(x) {
return x;
}// https://observablehq.com/@observablehq/text-color-annotations-in-markdown#textcolor
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= yiq(background) >= 0.6 ? "#111" : "white",
color = "0 1px",
padding = "4px",
borderRadius = 900,
fontWeight = "1em",
fontSize ...rest
= typeof style === "string" ? {background: style} : style;
} return htl.html`<span style=${{
,
background,
color,
padding,
borderRadius,
fontWeight...rest
}>${content}</span>`;
}
}// http://howardhinnant.github.io/date_algorithms.html#civil_from_days
function dote2doty(days = 719468) {
const era = Math.floor((days >= 0 ? days : days - 146096) / 146097),
= days - era * 146097,
dotc = Math.floor((dotc - Math.floor(dotc / 1460) + Math.floor(dotc / 36524) - Math.floor(dotc / 146096)) / 365);
yotc return [yotc + era * 400, dotc + Math.floor(yotc / 100) - yotc * 365 - Math.floor(yotc / 4)];
}function unix2doty(ms = 0) {
return dote2doty(ms / 86400000 + 719468)
}// https://howardhinnant.github.io/date_algorithms.html#days_from_civil
function doty2dote(year = 1969, doty = 0, zone = 0) {
const cycle = Math.floor((year >= 0 ? year : year - 399) / 400),
= year - cycle * 400;
yote return cycle * 146097 + yote * 365 + Math.floor(yote / 4) - Math.floor(yote / 100) + doty - zone
}function doty2deco(yearDoty = [1969, 306], zone = 0) {
const yd = dote2doty(doty2dote(yearDoty[0], Math.floor(yearDoty[1])));
return `${yd[0]}+${(yd[1]).toString().padStart(3, "0")}${
1].toString().includes(".") ? "." + (
yearDoty[1] > 0) ? (yearDoty[1] - zone).toString().split(".").pop()
(yearDoty[: [...(yearDoty[1] - zone).toString().split(".").pop()].map(
, i, a) => (i + 1 === a.length) ? 10 - e : 9 - e
(e.join("")
): ""
) }`
}function unix2deco(ms = 0) {
return doty2deco(unix2doty(ms));
;
}// http://howardhinnant.github.io/date_algorithms.html#days_from_civil
function greg2doty(month = 1, day = 1) {
return Math.floor(
153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1
(
)}// http://howardhinnant.github.io/date_algorithms.html#civil_from_days
function doty2greg(doty = 0) {
const m = Math.floor((5 * doty + 2) / 153);
return [Math.floor(m < 10 ? m + 3 : m - 9), doty - Math.floor((153 * m + 2) / 5) + 1];
}function doty2toty(doty = 306) {
= Math.floor(doty)
doty return (205 <= doty && doty < 295) ? ["Fall🍁", "Spring🌼"] :
110 <= doty && doty < 205) ? ["Summer☀️", "Winter❄️"] :
(20 <= doty && doty < 110) ? ["Spring🌼", "Fall🍁"] :
(0 <= doty && doty < 20) || (295 <= doty && doty <= 365)
(? ["Winter❄️", "Summer☀️"] : "Unknown"
}function doty2zodi(doty = 306) {
= Math.floor(doty)
doty return (20 <= doty && doty < 50) ? "Aries♈️" :
50 <= doty && doty < 81) ? "Taurus♉️" :
(81 <= doty && doty < 112) ? "Gemini♊️" :
(112 <= doty && doty < 144) ? "Cancer♋️" :
(144 <= doty && doty < 175) ? "Leo♌️" :
(175 <= doty && doty < 206) ? "Virgo♍️" :
(206 <= doty && doty < 236) ? "Libra♎️" :
(236 <= doty && doty < 266) ? "Scorpio♏️" :
(266 <= doty && doty < 296) ? "Sagittarius♐️" :
(296 <= doty && doty < 325) ? "Capricorn♑️" :
(325 <= doty && doty < 355) ? "Aquarius♒️" :
(355 <= doty && doty <= 365) || (0 <= doty && doty < 20)
(? "Pisces♓️" : "Unknown"
}function doty2month(doty = 0) {
const m = Math.floor((5 * doty + 2) / 153);
return Math.floor(m < 10 ? m + 3 : m - 9);
}function month2doty(month = 1) {
return Math.floor(
153 * (month > 2 ? month - 3 : month + 9) + 2) / 5
(
)}function doty2dotm(doty = 0) {
const m = Math.floor((5 * doty + 2) / 153);
return doty - Math.floor((153 * m + 2) / 5) + 1;
}
Variables
width
= {
unix while(true) {
yield Date.now();
}
}= {
tick let i = 0;
while (true) {
yield Promises.tick(864, ++i);
}
}= tick % 1e5
tickTime = unix2dote(unix).toString().split(".")[1].slice(0, 8)
barTime = barTime.slice(0, 2)
barCents = barTime[2]
barMils = barTime.slice(3, 5)
barBeats = barTime.slice(5)
barMb = unix2deco(unix).slice(0, 14)
deco = deco.slice(5)
doty = doty.slice(0, 3)
dotyDate = doty.slice(4)
dotyTime = doty.slice(0, 2)
dotyDek = doty[2]
dotyDotd = {
iso while(true) {
yield new Date().toISOString()
}
}= iso.slice(5, 10)
isoDate = iso.slice(11, 19)
isoTime = doty2toty(dotyDate)
season = doty2zodi(dotyDate)
zodiac = doty2toty(inputDoty)
inputDotySeason = doty2zodi(inputDoty)
inputDotyZodiac = Array.from({length: 366}, (_, i) => i) numbers
= setStyle(deco.slice(0, 4), d3.schemePaired[10])
styledYear = setStyle(dotyDate, d3.schemePaired[1])
styledDotyDate = setStyle(dotyTime, d3.schemePaired[2])
styledDotyTime = setStyle(dotyDate, d3.schemePaired[1])
styledDotyDate1 = setStyle(dotyTime, d3.schemePaired[2])
styledDotyTime1 = setStyle(tickTime, d3.schemePaired[2])
styledTickTime = setStyle(dotyDek, d3.schemePaired[0])
styledDek = setStyle(dotyDotd, d3.schemePaired[9])
styledDotd = setStyle(dotyTime.slice(0, 2), d3.schemePaired[7])
styledCent = setStyle(dotyTime[2], d3.schemePaired[6])
styledMil = setStyle(dotyTime.slice(3, 5), d3.schemePaired[11]) styledBeat
Set values
Deco
Observable code
Functions
:
copyqfunction unix2doty(ms = 0) {
const days = ms / 86400000 + 719468,
= days - (era = Math.floor((days >= 0 ? days : days - 146096) / 146097)) * 146097,
dote = Math.floor((dote - dote / 1460 + dote / 36524 - dote / 146096) / 365) + era * 400;
year return [year, days - Math.floor(year * 365 + year / 4 - year / 100 + year / 400)];
}
const [year, doty] = unix2doty(Date.now()),
= `${year.toString().padStart(4, "0")}+${
datetime = Math.floor(doty)).toString().padStart(3, "0")}.${
(day Math.round((doty - day) * 1e5)).toString().padStart(5, "0")}+0`;
(
copy(datetime)
copySelection(datetime)
paste()
function zone2hour(zone = "Z") {
return (zone = zone.toUpperCase()) == "Z" ? 0
: zone > "@" && zone < "J" ? zone.charCodeAt() - 64
: zone > "J" && zone < "N" ? zone.charCodeAt() - 65
: zone < "Z" && zone > "M" ? -(zone.charCodeAt() - 77)
: zone;
}function deco2doty(timestamp = "1969+306.00000Z") {
const arr = timestamp.toString().split(/(?=[+-]|[a-zA-Z])/, 3);
switch (arr.length) {
case 1: return [unix2doty(Date.now())[0], parseFloat(arr[0]), 0];
case 2: return (/^[a-zA-Z]+$/.test(arr[1]))
? [unix2doty(Date.now())[0], parseFloat(arr[0]), zone2hour(arr[1]) / 24]
: [parseFloat(arr[0]), parseFloat(arr[1]), 0];
;
}return [parseFloat(arr[0]), parseFloat(arr[1]), /^[a-zA-Z]+$/.test(arr[2])
? zone2hour(arr[2]) / 24
: parseFloat(arr[2].replace(/([+-])/, "$1\."))];
}function deco2deco(timestamp = "1969+306.00000Z") {
return doty2deco(deco2doty(timestamp))
}function doty2doty(year = 1969, doty = 0, zone = 0) {
return dote2doty(doty2dote(year, doty, zone));
}function doty2unix(year = 1969, doty = 306, zone = 0) {
return (doty2dote(year, doty, zone) - 719468) * 86400000;
}function doty2isoc(year = 1969, doty = 306, zone = 0) {
return new Date(doty2unix(year, doty, zone)).toISOString().split(".")[0]
}function deco2isoc(timestamp = "1969+306.00000Z") {
return doty2isoc(...deco2doty(timestamp))
}function unix2dote(ms = 0) {
return ms / 86400000 + 719468;
}function dote2year(days = 719468) {
const era = Math.floor((days >= 0 ? days : days - 146096) / 146097),
= days - era * 146097,
dotc = (dotc - Math.floor(dotc / 1460) + Math.floor(dotc / 36524) - Math.floor(dotc / 146096)) / 365;
yotc return yotc + era * 400;
}function doty2year(year = 1969, doty = 306, zone = 0) {
return dote2year(doty2dote(year, doty, zone));
}function unix2year(ms = 0) {
return dote2year(unix2dote(ms));
; }
Variables
= inputDatetime.getTimezoneOffset() * 60000
tzOffsetInMs = inputDatetime - tzOffsetInMs
utcDatetime = unix2deco(utcDatetime)
outputDatetimeDeco = outputDatetimeDeco.split("+")[1]
dtDoty = dtDoty.split(".")[0]
dtDotyDateRaw = dtDoty.split(".")[1].slice(0, 5)
dtDotyTimeRaw = unix2year(utcDatetime).toString().slice(0, 8)
outputDatetimeYear = dtDotyTimeRaw ? "." : ""
dtDotyTimeDelimiter = doty2year(...yd).toString().slice(0, 8)
outputDecoYear = doty2isoc(...yd)
outputIsoc = Math.ceil(unix2year(unix) * 1e3) / 1e3 yearDate
= setStyle(outputDatetimeDeco.split("+")[0], d3.schemePaired[10])
dtYear = setStyle(dtDotyDateRaw, d3.schemePaired[1])
dtDotyDate = dtDotyTimeRaw ? setStyle(dtDotyTimeRaw, d3.schemePaired[2]) : ""
dtDotyTime = setStyle(outputIsoc.split(/(?=-)/)[0], d3.schemePaired[10])
styledIsocYear = setStyle(outputIsoc.slice(outputIsoc.split(/(?=-)/)[0].length+1,outputIsoc.split(/(?=-)/)[0].length+6), d3.schemePaired[5])
styledIsocMd = setStyle(outputIsoc.slice(outputIsoc.split(/(?=-)/)[0].length+7), d3.schemePaired[4])
styledIsocTime = setStyle(outputDatetimeYear, d3.schemePaired[10])
styledOutputDatetimeYear = setStyle(outputDecoYear, d3.schemePaired[10]) styledOutputDecoYear
Dek
Observable code
Functions
function interval(range = [], options = {}) {
const [min = 0, max = 1] = range;
const {
= .001,
step = null,
label = [min, max],
value = ([start, end]) => `${start} … ${end}`,
format ,
color,
width,
theme= options;
} const __ns__ = DOM.uid('scope').id;
const css = `
#${__ns__} {
font: 13px/1.2 var(--sans-serif);
display: flex;
align-items: baseline;
flex-wrap: wrap;
max-width: 100%;
width: auto;
}
@media only screen and (min-width: 30em) {
#${__ns__} {
flex-wrap: nowrap;
width: 360px;
}
}
#${__ns__} .label {
width: 50px;
padding: 5px 0 4px 0;
margin-right: .5px;
flex-shrink: 0;
}
#${__ns__} .form {
display: flex;
width: 100%;
}
#${__ns__} .range {
flex-shrink: 1;
width: 100%;
}
#${__ns__} .range-slider {
width: 100%;
}
`;
const $range = rangeInput({min, max, value: [value[0], value[1]], step, color, width, theme});
const $output = html`<output>`;
const $view = html`<div id=${__ns__}>
${label == null ? '' : html`<div class="label">${label}`}
<div class=form>
<div class=range>
${$range}<div class=range-output style="display: inline-block;">${$output}</div>
</div>
</div>
${html`<style>${css}`}
`;
const update = () => {
const content = format([$range.value[0], $range.value[1]]);
if(typeof content === 'string') $output.value = content;
else {
while($output.lastChild) $output.lastChild.remove();
.appendChild(content);
$output
};
}.oninput = update;
$rangeupdate();
return Object.defineProperty($view, 'value', {
get: () => $range.value,
set: ([a, b]) => {
.value = [a, b];
$rangeupdate();
,
};
})
}= v => v == null ? null : typeof v === 'number' ? `${v}px` : `${v}`
cssLength function randomScope(prefix = 'scope-') {
return prefix + (performance.now() + Math.random()).toString(32).replace('.', '-');
}function rangeInput(options = {}) {
const {
= 0,
min = 100,
max = 'any',
step value: defaultValue = [min, max],
,
color,
width= theme_Flat,
theme = options;
} const controls = {};
const scope = randomScope();
const clamp = (a, b, v) => v < a ? a : v > b ? b : v;
const html = htl.html;
// Will be used to sanitize values while avoiding floating point issues.
const input = html`<input type=range ${{min, max, step}}>`;
const dom = html`<div class=${`${scope} range-slider`} style=${{
,
colorwidth: cssLength(width),
}>
} ${controls.track = html`<div class="range-track">
${controls.zone = html`<div class="range-track-zone">
${controls.range = html`<div class="range-select" tabindex=0>
${controls.min = html`<div class="thumb thumb-min" tabindex=0>`}
${controls.max = html`<div class="thumb thumb-max" tabindex=0>`}
`}
`}
`}
${html`<style>${theme.replace(/:scope\b/g, '.'+scope)}`}
</div>`;
let value = [], changed = false;
Object.defineProperty(dom, 'value', {
get: () => [...value],
set: ([a, b]) => {
= sanitize(a, b);
value updateRange();
,
};
})const sanitize = (a, b) => {
= isNaN(a) ? min : ((input.value = a), input.valueAsNumber);
a = isNaN(b) ? max : ((input.value = b), input.valueAsNumber);
b return [Math.min(a, b), Math.max(a, b)];
}const updateRange = () => {
const ratio = v => (v - min) / (max - min);
.style.setProperty('--range-min', `${ratio(value[0]) * 100}%`);
dom.style.setProperty('--range-max', `${ratio(value[1]) * 100}%`);
dom;
}const dispatch = name => {
.dispatchEvent(new Event(name, {bubbles: true}));
dom;
}const setValue = (vmin, vmax) => {
const [pmin, pmax] = value;
= sanitize(vmin, vmax);
value updateRange();
// Only dispatch if values have changed.
if(pmin === value[0] && pmax === value[1]) return;
dispatch('input');
= true;
changed ;
}setValue(...defaultValue);
// Mousemove handlers.
const handlers = new Map([
.min, (dt, ov) => {
[controlsconst v = clamp(min, ov[1], ov[0] + dt * (max - min));
setValue(v, ov[1]);
,
}].max, (dt, ov) => {
[controlsconst v = clamp(ov[0], max, ov[1] + dt * (max - min));
setValue(ov[0], v);
,
}].range, (dt, ov) => {
[controlsconst d = ov[1] - ov[0];
const v = clamp(min, max - d, ov[0] + dt * (max - min));
setValue(v, v + d);
,
}];
])// Returns client offset object.
const pointer = e => e.touches ? e.touches[0] : e;
// Note: Chrome defaults "passive" for touch events to true.
const on = (e, fn) => e.split(' ').map(e => document.addEventListener(e, fn, {passive: false}));
const off = (e, fn) => e.split(' ').map(e => document.removeEventListener(e, fn, {passive: false}));
let initialX, initialV, target, dragging = false;
function handleDrag(e) {
// Gracefully handle exit and reentry of the viewport.
if(!e.buttons && !e.touches) {
handleDragStop();
return;
}= true;
dragging const w = controls.zone.getBoundingClientRect().width;
.preventDefault();
e.get(target)((pointer(e).clientX - initialX) / w, initialV);
handlers
}function handleDragStop(e) {
off('mousemove touchmove', handleDrag);
off('mouseup touchend', handleDragStop);
if(changed) dispatch('change');
}.then(handleDragStop);
invalidation.ontouchstart = dom.onmousedown = e => {
dom= false;
dragging = false;
changed if(!handlers.has(e.target)) return;
on('mousemove touchmove', handleDrag);
on('mouseup touchend', handleDragStop);
.preventDefault();
e.stopPropagation();
e= e.target;
target = pointer(e).clientX;
initialX = value.slice();
initialV ;
}.track.onclick = e => {
controlsif(dragging) return;
= false;
changed const r = controls.zone.getBoundingClientRect();
const t = clamp(0, 1, (pointer(e).clientX - r.left) / r.width);
const v = min + t * (max - min);
const [vmin, vmax] = value, d = vmax - vmin;
if(v < vmin) setValue(v, v + d);
else if(v > vmax) setValue(v - d, v);
if(changed) dispatch('change');
;
}return dom;
}function formatDecimal(number) {
return number == 1 ? number : (Math.round(number * 100) / 100).toString().slice(1)
}= (data, filename = 'data.csv') => {
button if (!data) throw new Error('Array of data required as first argument');
let downloadData;
if (filename.includes('.csv')) {
= new Blob([d3.csvFormat(data)], { type: "text/csv" });
downloadData else {
} = new Blob([JSON.stringify(data, null, 2)], {
downloadData type: "application/json"
;
})
}return DOM.download(
,
downloadData,
filename"Download"
;
)
}function invert(arr) {
return invertDotd ? [...Array(10).keys()].map(n => !arr.includes(n)).map((x, i) => x ? i : null).filter(i => i !== null) : arr
}// https://observablehq.com/@chrispahm/toggle-switch-input-button
function toggleSwitch(options = { textOn: 'True', textOff: 'False' }) {
const button = html`<div class="button-switch""></div>`;
.innerText = options.textOn;
button.value = true;
button
.onclick = () => {
button.value = !button.value;
button.innerText = button.value ? options.textOn : options.textOff;
button.dispatchEvent(new CustomEvent("input"));
button;
}
addButtonStyle()
return button;
}// https://observablehq.com/@chrispahm/toggle-switch-input-button
function addButtonStyle() {
var el = document.createElement('style');
.setAttribute('type', 'text/css');
eldocument.head.appendChild(el);
var sheet = el.sheet;
function addRule(rule) {
try {
.insertRule(rule, sheet.cssRules.length);
sheetcatch (e) {
} console.warn('Error inserting rule', rule, e);
}
}addRule(
'.button-switch { -webkit-touch-callout: none;-webkit-user-select: none;-khtml-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;display: inline-block;-webkit-border-radius: 2px;-moz-border-radius: 2px;border-radius: 2px;background-color: #EFEFEF;padding: 0px 7px 0px 7px;text-align: center;border: 1px solid grey;width: auto;color: #1c1c1c;font-size: 14px;font-family: sans-serif;text-decoration: none; }'
;
)addRule(
'.button-switch:hover { background-color: #E5E5E5;border: 1px solid #454545; }'
;
)addRule(
'.button-switch:active { background-color: #f5f5f5;border: 1px solid grey; }'
;
)
}function setSpreadDotd(dotd) {
const dotdLength = dotd.length;
if (dotdLength === 10) {return ">"}
if (dotdLength === 0) {return ">0"}
if (dotdLength === 1) {return `${dotd[0]}>>1>9`}
const steps = dotd.map((x, i) => dotd[i + 1] - x)
.filter(i => !isNaN(i)),
= [];
result let split = 1;
for (const step of steps) {
if (step !== 1) {
.push([split, step - 1])
result= 1
split else {
} += 1
split
}
}.push(split)
resultconst flat = [].concat(...result),
= flat.reduce((a, b) => a + b, 0),
resultSum = [...new Set(flat)];
uniq return [dotd[0] ? dotd[0] : null, null].concat(JSON.stringify(uniq) === "[1]" ? [null, 1] : uniq.length === 2 && uniq.reduce((a, b) => a + b, 0) === 5 ? uniq : flat.concat(10 - resultSum).map((x, i) => i % 2 === 0 && x === 1 ? null : x)).join(">")
}function setSliceDotd(dotd) {
const dotdLength = dotd.length;
if (dotdLength === 10) {return ":"}
if (dotdLength === 0) {return ":0"}
const steps = dotd.map((x, i) => dotd[i + 1] - x)
.filter(i => !isNaN(i)),
= steps.reduce((a, b) => a + b, 0),
stepSum = [...new Set(steps.concat(10 - stepSum))];
uniq return [dotd[0] ? dotd[0] : null, null].concat(uniq.length === 1 ? uniq : JSON.stringify(steps) === "[1,4,1]" ? [1, 4] : JSON.stringify(steps) === "[1,1,3,1,1]" ? [1, 1, 3] : JSON.stringify(steps) === "[1,1,1,2,1,1,1]" ? [1, 1, 1, 2] : steps.concat(10 - stepSum).map(x => x === 1 ? null : x)).join(":");
}function encodeSlice(...args) {
return args.join().replaceAll(",", ":")
}function decodeSlice(s) {
let [start, stop, ...steps] = s.split(":").map(i => i === "" ? null : Number(i))
const step = steps[0] == null ? 1: steps[0];
= start == null && step >= 0 ? 0 : start == null && step < 0 ? 365 : start;
start = stop == null && step >= 0 ? 366 : stop;
stop return [start, stop, step]
}function decodeSpread(spread) {
return spread.replace(/^</, "366<").replace(/^>/, "0>")
.split(/(?=^\d|<|>)/).map(s => s.replace("<", "-")
.replace(">", "")).map(s => s === "" || isNaN(s) ? null : Number(s))
}function encodeSpread(args) {
return args.map(a => isNaN(a) ? null : a).join(">").replace(">-", "<")
}function slice(self, start, stop, steps, ...args) {
const len = self.length, result = [];
if (steps === 0 || len === 0) { return result };
= (!steps || steps.length === 0 ? [1] :
steps typeof steps === "number" ? [steps] :
typeof steps === "string" ? Array.from(steps, Number) :
.concat(args.map(i => i == null ? 1 : i));
steps)const stepCount = steps.length,
= steps.reduce((a, b) => a + b, 0);
stepSum if (stepSum === 0) { return result };
= Math.max(
start == null && stepSum > 0 ? 0 :
start == null && stepSum < 0 ? len - 1 :
start >= len ? len - 1 :
start < 0 ? start + len :
start , 0);
start= Math.max(
stop == null && stepSum > 0 ? len :
stop == null && stepSum < 0 ? -1 :
stop >= len ? len :
stop < 0 ? stop + len :
stop , -1);
stopfor (
let i = start, counter = -1;
> 0 ? i < stop : i > stop;
stepSum += steps[counter]
i
) {.push(self[i]);
result= (counter + 1) % stepCount;
counter ;
}return result;
}function spread(self, startOrStop, span, splitsAndSpaces, ...args) {
const len = self.length, result = [];
if (span === 0 || len === 0) { return result };
= (!splitsAndSpaces || splitsAndSpaces.length === 0 ? [1] :
splitsAndSpaces typeof splitsAndSpaces === "number" ? [splitsAndSpaces] :
typeof splitsAndSpaces === "string" ? Array.from(splitsAndSpaces, Number) :
.concat(args.map(i => i == null ? 1 : i));
splitsAndSpaces)const splitCount = splitsAndSpaces.length,
= splitsAndSpaces.reduce((a, b) => a + b, 0);
splitSpaceSum if (splitSpaceSum <= 0) { return result };
= Math.max(
startOrStop == null && span > 0 || startOrStop == null && span == null ? 0 :
startOrStop == null || startOrStop > len && span < 0 ? len :
startOrStop < 0 ? startOrStop + len :
startOrStop , 0);
startOrStop= span == null || startOrStop + span > len ? len - startOrStop :
span + span < 0 ? -startOrStop: span;
startOrStop const start = span > 0 ? startOrStop : startOrStop + span,
= span > 0 ? startOrStop + span : startOrStop;
stop for (let i = start, c = -1, arr = []; i < stop; i += splitsAndSpaces[c]) {
if ((c = (c + 1) % splitCount) % 2 === 0 && i + splitsAndSpaces[c] <= stop) {
if ((arr = Array.from({length: splitsAndSpaces[c]}, (_, j) => j + i).map(
=> self[index]).filter(item => item !== undefined)
index .length == splitsAndSpaces[c]) { result.push(arr) }
)
};
}return result;
}
Variables
= Array.from({length: intervals.length}, (_, i) => ([
nested
{label: `${i} or ${i+5}`,
duration: intervals[i][1] !== intervals[i][0] ? intervals[i][0] : 1,
group: "Rest"
,
}
{label: `${i} or ${i+5}`,
duration: intervals[i][1]-intervals[i][0],
group: "Work"
,
}
{label: `${i} or ${i+5}`,
duration: intervals[i][1] !== intervals[i][0] ? 1-intervals[i][1] : null,
group: "Rest"
}]))= [].concat(...nested)
durations = `
theme_Flat /* Options */
:scope {
color: #3b99fc;
width: 240px;
}
:scope {
position: relative;
display: inline-block;
--thumb-size: 15px;
--thumb-radius: calc(var(--thumb-size) / 2);
padding: var(--thumb-radius) 0;
margin: 2px;
vertical-align: middle;
}
:scope .range-track {
box-sizing: border-box;
position: relative;
height: 7px;
background-color: hsl(0, 0%, 80%);
overflow: visible;
border-radius: 4px;
padding: 0 var(--thumb-radius);
}
:scope .range-track-zone {
box-sizing: border-box;
position: relative;
}
:scope .range-select {
box-sizing: border-box;
position: relative;
left: var(--range-min);
width: calc(var(--range-max) - var(--range-min));
cursor: ew-resize;
background: currentColor;
height: 7px;
border: inherit;
}
/* Expands the hotspot area. */
:scope .range-select:before {
content: "";
position: absolute;
width: 100%;
height: var(--thumb-size);
left: 0;
top: calc(2px - var(--thumb-radius));
}
:scope .range-select:focus,
:scope .thumb:focus {
outline: none;
}
:scope .thumb {
box-sizing: border-box;
position: absolute;
width: var(--thumb-size);
height: var(--thumb-size);
background: #fcfcfc;
top: -4px;
border-radius: 100%;
border: 1px solid hsl(0,0%,55%);
cursor: default;
margin: 0;
}
:scope .thumb:active {
box-shadow: inset 0 var(--thumb-size) #0002;
}
:scope .thumb-min {
left: calc(-1px - var(--thumb-radius));
}
:scope .thumb-max {
right: calc(-1px - var(--thumb-radius));
}
`
= [
schedules .2, .8], [.2, .8], [0, 0], [0, 0], [0, 0]],
[[.3, .7], [.3, .7], [.3, .7], [0, 0], [0, 0]],
[[.35, .65], [.35, .65], [.35, .65], [.35, .65], [0, 0]],
[[.38, .62], [.38, .62], [.38, .62], [.38, .62], [.38, .62]],
[[
]= Array.from({length: 366}, (_, i) => i.toString().padStart(3, "0"))
dates = slice(dates, ...sliceString.split(":").map(i => i === "" ? null : Number(i)))
sliceArray = [].concat(...spread(dates, ...decodeSpread(spreadString))) spreadArray
Set values
Footnotes
Decalendar
: a calendar system that usesdeks
instead of months and weeks↩︎Declock
: a timekeeping system that uses fractional days instead of hours, minutes, and seconds↩︎doty
: day-of-the-year; an alternative to months, weeks, hours, minutes, and seconds↩︎doty
date: the integer part of thedoty
; an alternate to calendar dates and week dates↩︎doty
time: the fractional part of thedoty
; an alternate to hours, minutes and seconds↩︎dek
: a group of 10 days; the first two digits of theDecalendar
date; an alternate to month and weeks↩︎dotd
: one of the 10 days in adek
; the last digit of theDecalendar
date; an alternate to the day-of-the-month and day-of-the-week↩︎cent
: a hundredth (\(10^{-2}\)) of a day; the first 2 digits of thedoty
time; an alternate to quarter hours↩︎mil
: a thousandth (\(10^{-3}\)) of a day; the first 3 digits of thedoty
time; an alternate to minutes↩︎beat
: a hundred thousandth (\(10^{-5}\)) of a day; the first 5 digits of thedoty
time; an alternate to seconds↩︎fractional year date: a year and the proportion of that year that has passed; an alternative to
Decalendar
dates↩︎deco
: a date and time format consisting of a year and adoty
; analogous to ISO 8601 dates↩︎pent
: a group of 5 days; half of adek
↩︎pently
schedule: one of a set of workday and restday schedules for the 5 days of thepent
↩︎
Citation
BibTeX citation:
@online{laptev2024,
author = {Laptev, Martin},
title = {Observable},
date = {2024},
urldate = {2024},
url = {https://maptv.github.io/software/observable/},
langid = {en}
}
For attribution, please cite this work as:
Laptev, Martin. 2024. “Observable.” 2024. https://maptv.github.io/software/observable/.