%%{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 Measurement System
Introducing the Dec measurement system, which uses turns instead of months, weeks, hours, minutes, seconds, and degrees.
2026+064
0 Dec measurement system
This part of my website focuses on Dec, a measurement system that I created. All Dec measurements are based on turns. When measuring angles📐, a turn (t) represents a full⭕️circle and equals 360 degrees (°) or \(\underline\tau\), 2𝜋, or 900ϡ radians (rad). Geographic coordinates and compass🧭directions are angles and thus can — and should — be measured in turns instead of rad or °.
1 Longitude latitude course
Dec measures longitude in parallels (λ), latitude in meridians (m), and compass directions in roses🌹(r). To measure certain kinds of angles, Dec uses specific types of turns with distinct names like λ, m, or r. All turn types can be combined with metric prefixes, like deci, centi, or milli, to create turn submultiples, such as deciturns (dt), centiturns (ct), or milliturns (mt).
The table below⬇️provides the current longitude in milliparallels (mλ) and latitude in millimeridians (mm) of Points 0 and 1 on the map🗺️beneath the table. By default, Point 0 is at 800 mλ and 0 mm, near the Galápagos🏝️archipelago of Ecuador🇪🇨, and Point 1 is at 800 mλ and 100 mm, near the bottom of the Missouri bootheel in the United States🇺🇸.
To move the points, click the map or edit their coordinates in the table. The toggle✅inputs above⬆️the table add layers to the map️: country borders, a rainbow🌈colored grid of Dec graticules, a choropleth of Coordinated Universal Time (utc) time zones, and solar terminator shading with a yellow🟡dot denoting the point where the Sun☀️is directly overhead: mλ and mm.
Alongside the geographic coordinates of a point, each row of the table contains the course in milliroses (mr) we would need to maintain to travel🧳the shortest distance to the other point. The shortest distance is shown as orange🟠dots on the map️. The default courses in mr are 0 (North) from Point 0 to 1 and 500 (South) from Point 1 to 0.
2 Distance speed duration
Dec measures distance in taurs (c), speed in omegars (v), and time in years (y) and days (d). Each of these four turn types approximates (\(\approx\)) a physical property of the Earth🌍: c = \(\underline{\tau r}\) \(\approx\) its circumference, y \(\approx\) the duration of its orbit around the Sun️, d \(\approx\) the duration of its rotation on its axis, and \(\text c\over\text d\) = v = \(\underline{\omega r}\) \(\approx\) the speed of its rotation at the Equator.
At a speed of 0.5 v or 500 milliomegars (mv), we could travel the 0.1 c or 100 millitaurs (mc) between the default positions📍of Points 0 and 1 in 0.2 d or 200 millidays (md). The time required to travel between two points is the distance divided by the speed: mc ÷ v = md = c ÷ v = mc ÷ mv = d.
Interactive world map
viewof bordertoggle = labelToggle(Inputs.toggle, "Border", false, "bordertoggle")
viewof gridtoggle = labelToggle(Inputs.toggle, "Grid", false, "gridtoggle")
viewof utctoggle = labelToggle(Inputs.toggle, "UTC", false, "utctoggle")
viewof suntoggle = labelToggle(Inputs.toggle, "Sun", false, "suntoggle")
rstbtn.node();table = createTable([
{ Point: 0, Milliparallel: 800, Millimeridian: 0, Milliwindrose: 0 },
{ Point: 1, Milliparallel: 800, Millimeridian: 100, Milliwindrose: 500 },
], { headerEditable: false, appendRows: false })
// {Point: 0, Milliparallel: `${Math.floor(long2turn(Place_A[0], 3))}`, Millimeridian: `${Math.floor(lati2turn(Place_A[1], 3))}`, Milliwindrose: `${Math.floor(lati2turn(coor2bear(Place_A, Place_B)))}`},
// {Point: 1, Milliparallel: `${Math.floor(long2turn(Place_B[0], 3))}`, Millimeridian: `${Math.floor(lati2turn(Place_B[1], 3))}`, Milliwindrose: `${Math.floor(lati2turn(coor2bear(Place_B, Place_A)))}`},
// ], {headerEditable: false, appendRows: false})// https://observablehq.com/@d3/solar-terminator
// https://observablehq.com/@mbostock/time-zones
viewof coordinates = worldMapCoordinates([[turn2long(table.rows[1].cells[1].childNodes[0].innerText), turn2degr(table.rows[1].cells[2].childNodes[0].innerText % 250)], [turn2long(table.rows[2].cells[1].childNodes[0].innerText), turn2degr(table.rows[2].cells[2].childNodes[0].innerText % 250)], projection], [width, height * mapsize / 100])
//viewof coordinates = worldMapCoordinates([
// [turn2long(table.rows[1].cells[1].childNodes[0].innerText), turn2degr(table.rows[1].cells[2].childNodes[0].innerText % 250)],
// [turn2long(table.rows[2].cells[1].childNodes[0].innerText), turn2degr(table.rows[2].cells[2].childNodes[0].innerText % 250)],
// projection], [width, height])Color wheel compass
// https://observablehq.com/@maddievision/enneagram
quickRender(326, 326, context => {
const center = 163
const ringRadius = 140
const ringLineWidth = 4
// Ring
context.beginPath();
context.lineWidth = ringLineWidth
context.strokeStyle = "#ddd"
context.arc(center, center, ringRadius, 0, 2 * Math.PI);
context.stroke();
context.font = "Bold 16px Arial"
context.textAlign = 'center'
let octPoints = []
for (let i = 0; i < 8; i++) {
const xPhase = Math.sin(i / 8 * 2 * Math.PI)
const yPhase = Math.cos(i / 8 * 2 * Math.PI)
const x = center + ringRadius * xPhase
const y = center - ringRadius * yPhase
octPoints.push([x, y])
}
// Lines
octConnections.forEach(([a, b], i ) => {
const [x1, y1] = octPoints[a]
const [x2, y2] = octPoints[b]
const lineAngle = Math.atan2(y2 - y1, x2 - x1)
// Draw just short of the label circumference
const x2a = x2 - 28 * Math.cos(lineAngle)
const y2a = y2 - 28 * Math.sin(lineAngle)
const x1a = x1 + 28 * Math.cos(lineAngle)
const y1a = y1 + 28 * Math.sin(lineAngle)
context.lineWidth = ringLineWidth
context.strokeStyle = "#ddd"
context.beginPath();
context.moveTo(x2a, y2a);
context.lineTo(x1a, y1a);
context.stroke();
})
// Arrow Heads
octConnections.forEach(([a, b], i ) => {
const [x1, y1] = octPoints[a]
const [x2, y2] = octPoints[b]
const lineAngle = Math.atan2(y2 - y1, x2 - x1)
const xl = x2 - 88 * Math.cos(lineAngle - (15 / 360) * 2 * Math.PI)
const yl = y2 - 88 * Math.sin(lineAngle - (15 / 360) * 2 * Math.PI)
const xr = x2 - 88 * Math.cos(lineAngle + (15 / 360) * 2 * Math.PI)
const yr = y2 - 88 * Math.sin(lineAngle + (15 / 360) * 2 * Math.PI)
const x2a = x2 - 22 * Math.cos(lineAngle)
const y2a = y2 - 22 * Math.sin(lineAngle)
const x = x2 - 69 * Math.cos(lineAngle)
const y = y2 - 69 * Math.sin(lineAngle)
context.fillStyle = hsl8[i]
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.lineWidth = 1
context.beginPath();
context.moveTo(x2a, y2a);
context.lineTo(xl, yl);
context.lineTo(xr, yr);
context.lineTo(x2a, y2a);
context.fill();
context.stroke();
context.fillStyle = yiq(hsl8[i]) > 0.51 ? "#000" : "white"
context.fillText(["N", "NE", "E", "SE", "S", "SW", "W", "NW"][i], x, y + 6)
})
// Labels
octPoints.forEach(([x, y], i) => {
context.lineWidth = 1
context.fillStyle = hsl8[i]
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.beginPath();
context.arc(x, y, 22, 0, 2 * Math.PI);
context.fill();
context.stroke();
context.fillStyle = yiq(hsl8[i]) > 0.51 ? "#000" : "white";
context.fillText(["N", "NE", "E", "SE", "S", "SW", "W", "NW"][i], x, y + 6)
})
})// https://observablehq.com/@maddievision/enneagram
quickRender(326, 326, context => {
const center = 163
const ringRadius = 140
const ringLineWidth = 4
// Ring
context.beginPath();
context.lineWidth = ringLineWidth
context.strokeStyle = "#ddd"
context.arc(center, center, ringRadius, 0, 2 * Math.PI);
context.stroke();
context.font = "Bold 24px Arial"
context.textAlign = 'center'
let decPoints = []
for (let i = 0; i < 10; i++) {
const xPhase = Math.sin(i / 10 * 2 * Math.PI)
const yPhase = Math.cos(i / 10 * 2 * Math.PI)
const x = center + ringRadius * xPhase
const y = center - ringRadius * yPhase
decPoints.push([x, y])
}
// Lines
decConnections.forEach(([a, b], i ) => {
const [x1, y1] = decPoints[a]
const [x2, y2] = decPoints[b]
const lineAngle = Math.atan2(y2 - y1, x2 - x1)
// Draw just short of the label circumference
const x2a = x2 - 28 * Math.cos(lineAngle)
const y2a = y2 - 28 * Math.sin(lineAngle)
const x1a = x1 + 28 * Math.cos(lineAngle)
const y1a = y1 + 28 * Math.sin(lineAngle)
context.lineWidth = ringLineWidth
context.strokeStyle = "#ddd"
context.beginPath();
context.moveTo(x2a, y2a);
context.lineTo(x1a, y1a);
context.stroke();
})
// Arrow Heads
decConnections.forEach(([a, b], i ) => {
const [x1, y1] = decPoints[a]
const [x2, y2] = decPoints[b]
const lineAngle = Math.atan2(y2 - y1, x2 - x1)
const xl = x2 - 79 * Math.cos(lineAngle - (15 / 360) * 2 * Math.PI)
const yl = y2 - 79 * Math.sin(lineAngle - (15 / 360) * 2 * Math.PI)
const xr = x2 - 79 * Math.cos(lineAngle + (15 / 360) * 2 * Math.PI)
const yr = y2 - 79 * Math.sin(lineAngle + (15 / 360) * 2 * Math.PI)
const x2a = x2 - 22 * Math.cos(lineAngle)
const y2a = y2 - 22 * Math.sin(lineAngle)
const x = x2 - 60 * Math.cos(lineAngle)
const y = y2 - 60 * Math.sin(lineAngle)
context.fillStyle = hsl10[i]
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.lineWidth = 1
context.beginPath();
context.moveTo(x2a, y2a);
context.lineTo(xl, yl);
context.lineTo(xr, yr);
context.lineTo(x2a, y2a);
context.fill();
context.stroke();
context.fillStyle = yiq(hsl10[i]) > 0.51 ? "#000" : "white"
context.fillText(i, x, y + 8)
})
// Labels
decPoints.forEach(([x, y], i) => {
context.lineWidth = 1
context.fillStyle = hsl10[i]
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.beginPath();
context.arc(x, y, 22, 0, 2 * Math.PI);
context.fill();
context.stroke();
context.fillStyle = yiq(hsl10[i]) > 0.51 ? "#000" : "white";
context.fillText(i, x, y + 8)
})
})// https://observablehq.com/@pjedwards/compass-rose-as-legend-with-colors
svg`<svg width="${size}" height="${size}" viewBox="${-size/2} ${-size/2} ${size} ${size}">
<g transform='rotate(${Math.round(-colorD * .36)})'>
${repeat(tick(radius, 5, '#434343'), 5 * 4 * 10)}
${repeat(tick(radius, 8), 10 * 4)}
${repeat(`<path d="M 0,-${radius+12} l 3,10 l -6,0 z" fill="black" stroke="black" stroke-width="1"/>`, 4, 0)}
${repeat(`<path d="M 0,-${radius+12} l 3,10 l -6,0 z" fill="white" stroke="black" stroke-width="1"/>`, 4, 45)}
<circle r="${radius}" fill="#d3d3d3" stroke="#434343" stroke-width="3" />
${repeat(directionMarker(radius+14, 24), 4, 0)}
${repeat(directionMarker(radius+12, 24), 4, 45)}
${repeat(turnMarker(radius+14, 32), 4, 0)}
${repeat(turnMarker(radius+12, 32), 4, 45)}
${repeat(pie(radius-margin/2, 2 * Math.PI * (radius-margin/2) / deccolors.length / 2, 1, deccolors), deccolors.length, 360/deccolors.length)}
</svg>
`// https://observablehq.com/@paavanb/progressive-color-picker
{ const input = Inputs.range([0, 1000], { label: "Hue", value: 0, step: 1 })
input.value = initialHSL[0]
input.oninput = (evt) => onUpdateHSL(dec2hue(evt.currentTarget.value / 1000), colorS / 1000, colorL / 1000)
return Inputs.bind(input, viewof colorD)
}Course color table
| mr🧭 | c°🧭 | h°🎨 | hex🎨 | |
|---|---|---|---|---|
| NE | 125 | 45 | 44 | fb0 |
| E | 250 | 90 | 68 | df0 |
| SE | 375 | 135 | 96 | 6f0 |
| S | 500 | 180 | 180 | 0ff |
| SW | 625 | 225 | 216 | 06f |
| W | 750 | 270 | 264 | 60f |
| NW | 875 | 315 | 292 | d0f |
| N | 0 | 0 | 0 | f00 |
The color🎨wheel compass above indicates both a hue in mt and a course in mr. We can convert the hue to HSL and HSV degrees (h°) and the course to compass degrees (c°): 25 mr = 9 c°. To rotate🔄the color wheel compass, use the “Hue” range🎚️and hue bar inputs beneath it or change the course from Point 0 to 1 by interacting with the table or map️ above.
3 Red green blue (rgb)
The table beneath the hue bar compares the current Point 0 to 1 course in its top row with the cardinal and intercardinal directions. Together, the range inputs underneath the hue bar form a “hue saturation lightness” (hsl) triplet. Like “red green blue” (rgb) or hexadecimal (hex) triplets, hsl triplets specify a full-fledged color instead of just a hue.
Color labels🏷️provide a general idea of angular measure, regardless of the metric prefixes or units we use. Therefore, we can reuse♻️colors across many different contexts. Most often, red designates starting points, like North (0 mr) and Longitude 0 (0 mλ), and cyan denotes midpoints, such as South (500 mr) and Longitude 5 (500 mλ).
The Equator (0 mm) is the major latitude midway between the South (-250 mm) and North (250 mm) Poles. Unlike the Equator, the Tropics of Cancer♋(65 mm) and Capricorn♑️(-65 mm) and the Arctic (250 mm – 65 mm = 185 mm) and Antarctic (65 mm – 250 mm = -185 mm) Circles are defined by the axial tilt of the Earth🌏: 65 mt.
4 Dec time zones
Enable the “Grid” toggle input to see Latitudes -2 (-200 mm), -1 (-100 mm), 0 (0 mm), 1 (100 mm), and 2 (200 mm) on the map above along with the ten major longitudes that divide the Earth🌎into the ten Dec time zones. Notably, Longitude 0 is the major longitude that functions as both the Prime Meridian and International Date Line in Dec.
Like the ten major longitudes that separate them, Dec time zones are numbered 0 to 9. Based on its current deciparallel (dλ) longitude, , Point 0 on the map above is in Zone . The number assigned to each time zone is its offset from Zone 0 in decidays (dd). To obtain the dd offset at a location, we floor its dλ longitude: ⌊⌋ = .
Each Dec time zone is 1 dλ wide and 0.5 m long. While 1 m is always ~1 c long, the length of a λ varies by latitude. At the Equator, 1 λ is ~1 c long. At the North or South Pole, the length of a λ is zero. The approximate c length of a λ is the cosine of its latitude in m, rad, or °, depending on the input requirement of our cosine function: cos() = .
5 Date and time
Dec dates consist of a “year of era” (yoe) and a “day of year” (doy), whereas Dec times are composed of a “time of day” (tod) and a “time zone offset” (tzo). In Zone 0, the current date is + and the current time is -0. Color labels make it easier to visually parse the date and time that make up a Dec snap🫰: +-0.
6 Millenium year day
Yoe color labels are based on “year of millenium” (yom) values. At the start of every millennium, the yom is 0. Halfway through a millenium, the yom is 500 y. Doy color labels are based on milliyears (my). Every year starts on Day 0 (0 my). The midyear point (500 my) is noon (500 md) on Day 182 in common years and midnight (0 md) on Day 183 in leap years.
7 Day of dek (dod)
Each doy also has two components. The first two digits of a three-digit doy represent a group of ten days called a decaday (dek). The last digit of a doy is the “day of dek” (dod). In Dec, deks are used instead of months and weeks. Likewise, Dec uses dod in lieu of days of month (dom) and days of week (dow). In Zone 0, it is currently Dek and Dod .
8 Zone equatorial meter (zem)
Apart from c, Dec also measures distance using a unit called the zone equatorial meter (zem). The width of a Dec time zone at the Equator is approximately ten million (~107) zem (z). Similarly, the distance from the Equator to one of the Poles is ~107 meters. In other words, a decimeridian (dm) is ~107 z long and a quarter meridian is ~107 meters long.
9 Length area volume
You can approximate a z using your hands🤲. With your palms flat on a table in front of you and the tips of your thumbs👍touching, the maximum distance between the tips of your pinkies is ~1 z. When you spread out the fingers on one hand✋or do the “call me”, “drink”, or “shaka”🤙gesture, your thumb and pinky tips are ~0.5 z apart.
To visualize a square zem (z²), imagine four people standing in a circle, facing inward, each with their right hand placed on top of the elbow of the person to their right. Alternatively, two people can stand in front of each other and raise their arms💪, placing one hand on the elbow of the other person and the other hand on their own elbow.
You can approximate a z² yourself by sitting in a chair🪑or standing🧍with your knees and feet🦶1 z, 4 decimeters, or 16 inches apart, which is probably about the width of your hips or shoulders. The z² will be between your shins, its top will be below your knees, and its bottom will be either above your ankles or feet, depending on your height.
10 Typical seat height
According to dimensions.com, 115 centizem (cz) is the typical seat height for both men and women in age range of 25 to 45 y. A box📦that is the size of a cubic zem (z³) would likely fit under a typical chair or in between the shins of two people sitting in front of each other with their knees and feet 1 z apart and their legs🦵bent at right angles (25 ct).
11 Perpetually setting sun
In Slovak🇸🇰, zem means Earth. This is fitting because all Dec units are based on physical attributes of the Earth. At the Equator, the Earth rotates on its axis at a speed of ~1.00224 v. If we could indefinitely maintain this speed while flying West in an airplane✈️towards the setting sun️, we would be able to perpetually fly into the sunset🌅.
12 Airplane cruising speed
To travel fast enough for a perpetual sunset, the airplane would need to surpass the speed of sound🔊(sos), which at 15 ° Celsius and 1 standard atmosphere is 0.735048 v or Mach 1. Mach numbers are relative to the sos, which varies greatly by air temperature and pressure. The cruising speed of a Boeing 747 is ~0.54 v or Mach ~0.85.
The highway🛣️speed of a car🚗is roughly tenfold slower than the cruising speed of an airplane️. If we are driving on a highway at a speed of 50 mv and our exit is 1000 z away, we will have 20 centimillidays (cmd) until we have to exit the highway️. To ensure we do not miss our exit, we can periodically check a countdown of the remaining z: .
13 Centimilliday (cmd)
Dec refers to cmd as beats (b) because they are similar in duration to heart❤️beats or musical beats. In Dec, 1 d = 100 centiday (cd) = 105 b = 106 microdays (µd), 1 mc = 100 kilozem (kz) = 105 z = 106 decizem (dz) = 106 nanotaurs (nc), and therefore, 1 mv = \(\text{mc}\over\text d\) = \(\text {kz}\over\text {cd}\) = \(\text z\over\text b\) = \(\text {dz}\over\text{µd}\) = \(\text {nc}\over\text{µd}\). A cd is 96% of a quarter hour and a b is 86.4% of a second.
14 Heart rate tempo
A normal resting heart rate is between 100 and 166.6 b per md (bpm). The unofficial anthem of the Dec measurement system, “Turn the beat around”, has a tempo of 188.64 bpm, which corresponds to the allegro tempo marking. A Dec clock⏰ticks at a rate of 100 bpm, \(\text b^{-1}\), \(1\over\text b\), 1 inverse beat, or 1 perbeat (p), which is 1.15740 times more frequent than a Hertz.
15 Frequency period wavelength
Dec uses p, b, and z, often with metric prefixes, to measure the frequency, period, and wavelength, respectively, of a sound or light wave. The equations below show how frequency, period, and wavelength are related to each other and to speed. The speed of light (sol) is roughly 647.551657 kiloomegars (kv), which is about 881 thousand times faster than the sos.
\[\text{frequency} = \text{speed} \div \text{wavelength} = 1 \div \text{period}\]
\[\text{period} = \text{wavelength} \div \text{speed} = 1 \div \text{frequency}\]
\[\text{wavelength} = \text{speed} \times \text{period} = \text{speed} \div \text{frequency}\]
The frequency range of the visible spectrum of light is ~345.6 to ~914.4 teraperbeats (Tp). The range of sound frequencies which can be audible👂for humans is ~10.368 to ~24192 p. The period and wavelength that correspond to the frequency chosen by the range input below are 1000 ÷ p = millibeats (mb) and 735.048 mv ÷ p = z.
16 Decioctave octave note
In addition to p, the limits of human hearing can also be expressed in decioctaves (do). A do is a tenth of an octave (o). An o is a two-fold change in frequency. The approximate audible range for humans is 3 to 103 do. The approximate range of an 88-key piano🎹is 23.76 to 3616.64 p or 8 to 80 do. The equations and code below convert between o and p.
\[\text{o} = \log_2\!\left(\frac{\text{p}}{14.1275}\right)\]
\[\text{p} = 14.1275 \times 2^{\text{o}}\]
perbeat <- \(o) 14.1275 * 2^o
octave <- \(p) log2(p / 14.1275)
octave(113.02)[1] 3
perbeat(3)[1] 113.02
17 Color and sound
Each do has a corresponding musical note (n) that determines its color label. The idea of linking colors and musical notes dates back the 1704 book by Isaac Newton entitled Optiks1. On 2025+080, I read The Color of Sound by Clint Goss2, which presents a method of associating musical notes with colors by matching sound and light frequencies.
\[\text{n} = \text{o \href{https://en.wikipedia.org/wiki/Modulo#:~:text=returns%20the%20remainder%20or%20signed%20remainder%20of%20a%20division}{mod} 1} \times 10\]
The lollipops🍭in the chart below represent the ten Dec notes in Octave 4. The lollipops and the do values above them are labeled with the ten Dec colors. Beneath each lollipop is its frequency rounded to the nearest p. Click or tap each lollipop to hear its associated sound. The chart demonstrates that data points can be labeled with both color and sound.
// https://observablehq.com/@mcmire/tone-map
{
const points = origs.map((orig, i) => ({
origin: orig,
x: orig,
ratio: Fraction(1, 1),
label: labels[i],
color: colors[i],
freq: hertz[i],
alwaysShowLabel: true
}));
return renderGraph(points, {
// axisTextColor: window.darkmode ? "#FFF" : "#000",
xAxis: {
ticks: origs.map(Math.round)
},
});
}18 Système international d’unités
As a scientist of European origin, I have a strong preference for the International System of Units (SI) over the United States🇺🇸(US) customary measurement system. Nevertheless, having grown up in the US, I understand the animosity towards unfamiliar measurement units expressed by Grandpa Simpson in “A Star Is Burns”, Season 6 Episode 18 of the Simpsons.
19 US customary units
Dec redefines US customary units to facilitate conversion with SI and Dec units. The values in the first column of unit conversion table below are approximate fold changes from original to redefined US customary units. A fold change of 1 means 0 change is required for alignment with the SI and Dec units shown in the second and third column, respectively.
Unlike Dec and SI, the US customary measurement system does not use metric prefixes to scale units by powers of ten. Redefined US customary units can serve as convenient reference points and provide intuitive names for certain fractions and multiples of Dec and SI units. For example, after being redefined, one hand is equal to a decimeter or a quarter z.
Length and distance
The unidimensional (1D) units in the table below can be divided into two groups: human- or horse-based length units and surveying distance units. A horse🐴length is about as long as a cruiser or touring motorcycle🏍️, approximately equivalent to the height of the tallest basketball🏀players, and roughly half of the length of a compact to mid-size car.
| US 1D units | zem | meter |
|---|---|---|
| 0.9843 inches | 0.0625 | 0.025 |
| 0.9843 palms | 0.1875 | 0.075 |
| 0.9843 hands | 0.25 | 0.1 |
| 0.9843 shaftments | 0.375 | 0.15 |
| 0.9942 links | 0.5 | 0.2 |
| 0.9843 spans | 0.5625 | 0.225 |
| 0.9843 feet | 0.75 | 0.3 |
| 0.9843 steps | 1.875 | 0.75 |
| 0.9843 yards | 2.25 | 0.9 |
| 0.9843 fathoms | 4.5 | 1.8 |
| 1 horse length | 6 | 2.4 |
| 0.9942 rods | 12.5 | 5 |
| 0.9942 chains | 50 | 20 |
| 0.9942 furlongs | 500 | 200 |
| 0.9942 miles | 4000 | 1600 |
| 0.9942 leagues | 12000 | 4800 |
Miles per hour
When we divide a 1D unit by a time unit, we get a speed unit. A mile per hour is very close to a z per b and a knot, which is used to measure the speed of aircraft and watercraft, is almost exactly the same as 10/9 z per b. Dec refers to 10/9 z as a cubit or ell (ℓ). An mv can also be expressed as one ℓ per Dec second (s). There are 86400 SI seconds or 90000 s in one day.
| US speed units | mv | km/hour |
|---|---|---|
| 0.9448 inches/second | 0.05625 | 0.09375 |
| 0.9448 feet/second | 0.675 | 0.675 |
| 0.9942 miles/hour | 0.96 | 1.6 |
| 0.9448 yards/second | 2.025 | 3.375 |
Are hectare acre
If we raise a 1D unit to the second power, we get a bidimensional (2D) area unit. A z² is 1 hexamilliare (x), 16 square (sq.) decimeters (dm²), ~0.1975 Dec sq. yards, 1.7 Dec sq. feet, or 256 Dec sq. inches. A sq. kilozem (kz²) is 1 hexakilare, 16 hectares, 1600 ares, 40 Dec acres, 0.16 sq. kilometers (km²), 0.0625 Dec sq. miles, 106 z², or 1 megahexamilliare (Mx).
| US 2D units | cz² | cm² |
|---|---|---|
| 0.9688 sq. inches | 39.0625 | 6.25 |
| 0.9688 sq. feet | 5625 | 900 |
| 0.9688 sq. yards | 50625 | 8100 |
| 0.9884 acres | 25 × 107 | 4 × 107 |
| 0.9884 sq. miles | 16 × 1010 | 256 × 108 |
Drop wineglass keg
US tridimensional (3D) volume units tend to scale by powers of two. A cubic (cu.) decizem (dz³) is like a double shot, either of espresso☕️or of liquor🥃, and is equal to 1 cu. nanotaur (nc³), 1 Dec wineglass (🍷glass), 2 Dec ounces (u), 4 Dec tablespoons (table🥄), 64 cu. centimeters (cm³), 64 milliliters (mL), 1000 Dec drops (g), or 1000 cu. centizem (cz³).
| US 3D units | cz³ | cm³ |
|---|---|---|
| 1.2549 drops | 1 | 0.064 |
| 0.9535 cu. inches | 244.140625 | 15.625 |
| 1.0821 table🥄 | 250 | 16 |
| 1.0821 ounces | 500 | 32 |
| 1.0821 🍷glasses | 1000 | 64 |
| 0.9468 cups | 3500 | 224 |
| 0.9468 pints | 7000 | 448 |
| 0.9468 quarts | 14000 | 896 |
| 0.9468 gallons | 56000 | 3584 |
| 0.9535 cu. feet | 421875 | 27000 |
| 1.0735 kegs | 1000000 | 64000 |
| 1.0735 barrels | 2000000 | 128000 |
| 1.0735 hogsheads | 4000000 | 256000 |
| 0.9535 cu. yards | 11390625 | 729000 |
Grain pound ton
Dec and SI measurements of mass are based on volumes of water🌊. A dz³ of water weighs \(1\over7\) Dec pounds, 64 grams, or 1000 Dec grains (g). One u of water weighs \(1\over14\) Dec pounds, 500 Dec g, or 32 grams. In Dec, g is short for granum and gutta, the Latin words for grain and drop, respectively. Similarly, u originates from uncia, the Latin word for “a twelfth”.
| US mass units | grain | gram |
|---|---|---|
| 0.9877 grains | 1 | 0.064 |
| 0.96 carats | 3 | 0.192 |
| 1.1288 ounces | 500 | 32 |
| 0.9877 pounds | 7000 | 448 |
| 0.9877 tons | 14000000 | 896000 |
Body mass index (bmi)
A z³ is 1 keg. A keg of water weighs 64 kilograms, 128 Dec pounds, or a 1000 Dec kilograins (kg). If Leonardo da Vinci’s Vitruvian Man were 4 z tall, we could measure 1 z from his knees to his feet or from his elbows to his fingertips. If he also weighed 1000 Dec kg, his body mass index (bmi) would be 62.5 kg per x (\(\text {kg}\over\text x\)) or 25 kilograms per m² (\(\text {kilogram}\over\text m^2\)).
A normal bmi ranges from 46.25 to 62.5 \(\text {kg}\over\text x\) or 18.5 to 25 \(\text {kg}\over\text m^2\). An obese person has a bmi above 75 \(\text {kg}\over\text x\) or 30 \(\text {kg}\over\text m^2\). Severe or morbid obesity is defined as a bmi above 100 \(\text {kg}\over\text x\) or 40 \(\text {kg}\over\text m^2\). A person with the weight and height selected by the range inputs below would be considered : kg ÷ x = \(\text {kg}\over\text x\) = kilograms ÷ m² = \(\text {kilograms}\over\text m^2\).
Centizem centimeter inch
The longest length depicted in the image of a ruler📏below is 1 dz, 1 nc, 4 centimeters, or \(8\over5\) Dec inches, and the shortest length is \(1\over2\) mz, \(1\over5\) millimeters, \(1\over125\) Dec inches, or \(1\over127\) US customary inches. A US customary inch is \(127\over2\) mz, \(127\over5\) millimeters, or \(127\over125\) Dec inches. A Dec inch is \(5\over2\) centimeters. A centimeter is \(5\over2\) cz. A z is 4 decimeters or 16 Dec inches.
20 Claude Boniface Collignon
In 1788, Claude Boniface Collignon proposed measuring length in dz or nc and tracking time in deks, dd, md, µd, and nanodays (nd)3. On 2025+039, while searching for units akin to c and z, I noticed the definition of a zem, 1 z = 10-8 c = 40 centimeters, in a table of ten possible length units from an arxiv article entitled “Why does the meter beat the second?”4.
Summary
This article introduces the Dec measurement system and describes how Dec uses metric prefixes and the properties of the planet Earth to define units based on turns for geographic coordinates, compass directions, dates, times, speeds, distances, areas, volumes, and weights. Each unit has a unique name, such as λ, m, r, y, d, b, v, c, z, or x.
Dec attempts to bridge the gap, improve interoperability, and faciliate conversion between the US customary and SI measurement systems by redefining certain units. Redefinition of US customary units makes human-based length units ~1.58% shorter and surveying distance units ~0.58% shorter. Dec also redefines SI seconds to be 4% shorter.
Dec color labels can convey an impression of a value at a glance👀. Dec sound labels allow us to estimate a value without even having to look at it🙈. Both types of labels can help avoid confusion when decimal separators appear, disappear, or move due to a measurement unit change such as the addition, removal, or replacement of a metric prefix.
Next
Now that you have had a taste👅of Dec, I hope that you are hungry🤤for more! If so, dive🤿deeper by reading my article on Dec dates, times, and snaps. My filter and include articles discuss the Quarto publishing system and how I customize my Quarto website to display a Dec snaps in the navigation bar and Dec dates in the article list and title blocks.
%%{init: {'theme': 'default', 'themeVariables': { 'fontSize': '32px'}}}%%
flowchart LR
A[Dec]-->B[date]-->C[time]-->D[snap]-->E[span]
Z[ ]:::empty~~~F[Quarto]-->G[filter]-->H[include]-->I[script]
classDef empty width:0px;
click A "/dec"
click B "/dec/date"
click C "/dec/time"
click D "/dec/snap"
click E "/dec/span"
click F "/quarto"
click G "/quarto/filter"
click H "/quarto/include"
click I "/quarto/script"
Cite
Of the bibliography file formats supported by Quarto, I recommend yaml. The yaml bibliography file shown below contains bibliographic records (metadata) about the article you are currently reading and the article entitled chrono-Compatible Low-Level Date Algorithms in which Howard Hinnant (2021+185) describes the algorithms underlying Dec dates.
ref.yml
references:
- id: hinnant2021date
author:
- family: Hinnant
given: Howard
title: [<code>chrono</code>]{.nocase}-Compatible Low-Level Date Algorithms
url: https://howardhinnant.github.io/date_algorithms
issued:
literal: 2021+185
- id: laptev2026dec
author:
- family: Laptev
given: Martin
title: Dec Measurement System
url: https://maptv.github.io/dec
issued:
literal: 2026+064Quarto configuration files, such as _quarto.yml and _metadata.yml, are written in yaml. Quarto input files, including Quarto markdown, Jupyter notebook, markdown, and specially formatted script files, can start with a yaml header. Therefore, we could put the metadata above directly into a Quarto configuration or input file rather than into a bibliography file.
As an alternative to yaml, I suggest the BibTeX format. The BibTeX bibliography file below can be used by Quarto equally as well as the yaml bibliography file above. Regardless of the bibliography file format we choose, Quarto configuration and input files require that we store the path to our bibliography file, or our list of bibliography file paths, in yaml format.
ref.bib
@misc{hinnant2021date,
author = "Howard Hinnant",
title = "\texttt{chrono}-Compatible Low-Level Date Algorithms",
url = "https://howardhinnant.github.io/date_algorithms",
year = 2021+185
}
@misc{laptev2026dec,
author = "Martin Laptev",
title = "Dec Measurement System",
url = "https://maptv.github.io/dec",
year = 2026+064
}In addition to storing metadata in a bibliography file, we can keep instructions regarding how to style citations and references in a Citation Style Language (csl) file. If we do not provide a csl file, Quarto will follow the Chicago Manual of Style when processing parenthetical citations: (Hinnant 2021+185), narrative citations: (2021+185), and references:
Hinnant, Howard. 2021+185. chrono-Compatible Low-Level Date Algorithms. https://howardhinnant.github.io/date_algorithms.html.
When provided with nature.csl, american-medical-association.csl, or a similar csl file, Quarto will produce superscript numeric citations, which look just like Quarto footnotes: 5. Unlike Quarto citations, Quarto footnotes do not require any additional files or configuration. A Quarto output file can have both a Footnotes and References section.
Observable notebooks
In alphabetical order below, you will find a list of the Observable computational notebooks that inspired many of the visualizations above and thus deserve their own section before the references and footnotes further below. On the Observable website, you can search for other awesome Observable notebooks, read the blog, or watch webinars and other videos.
- Armstrong, Zan. 2023+057. Text color annotations in markdown. https://observablehq.com/@observablehq/text-color-annotations-in-markdown.
- Bostock, Mike. 2020+335. Time Zones. https://observablehq.com/@mbostock/time-zones.
- Bostock, Mike. 2022+037. Solar Terminator. https://observablehq.com/@d3/solar-terminator.
- Bostock, Mike. 2023+314. Input: Table. https://observablehq.com/@observablehq/input-table.
- Edwards, Paul. 2022+171. Compass Rose as legend with colors. https://observablehq.com/@pjedwards/compass-rose-as-legend-with-colors.
- Freedman, Dylan. 2017+345. Sounds. https://observablehq.com/@freedmand/sounds.
- Gordon, Marcus A.. 2018+288. Wavelengths and Spectral Colours. https://observablehq.com/@magfoto/wavelengths-and-spectral-colours.
- Harmath, Dénes. 2018+104. ABC. https://observablehq.com/@thsoft/abc.
- Johnson, Ian. 2021+121. Draggable World Map Coordinates Input. https://observablehq.com/@enjalot/draggable-world-map-coordinates-input.
- Lim, Maddie. 2018+330. Enneagram. https://observablehq.com/@maddievision/enneagram.
- Paavanb. 2024+006. Progressive Color Picker. https://observablehq.com/@paavanb/progressive-color-picker.
- Patel, Amit. 2021+290. Compass Rose. https://observablehq.com/@paavanb/progressive-color-picker.
- Pettiross, Jeff. 2024+150. Categorical color scheme test tool. https://observablehq.com/@observablehq/categorical-palette-tool
- Rieder, Lukas. 2023+032. Editable table. https://observablehq.com/@parlant/editable-table.
- Rivière, Philippe. 2022+259. Add a class to an observable input. https://observablehq.com/@recifs/add-a-class-to-an-observable-input--support.
- Rivière, Philippe. 2023+330. D3 Projections. https://observablehq.com/@fil/d3-projections.
- Winkler, Elliot. 2019+070. Illustrating harmony with the harmonic series. https://observablehq.com/@mcmire/illustrating-harmony-with-the-harmonic-series.
- Yamahata, Christophe. 2021+119. Great circle: shortest distance between two locations on Earth 🌏. https://observablehq.com/@christophe-yamahata/great-circle-shortest-distance-between-two-locations-on-ea.
tableify = import("https://cdn.skypack.dev/tableify@1.1.1?min")
xss = import("https://cdn.skypack.dev/xss@1.0.14?min")
function createCellDiv(value, max) {
return `<div style="
width: ${Math.abs(value) / max}%;
float: left;
padding: 0px 0px 0px 2px;
text-indent: 2px;
box-sizing: border-box;
overflow: visible;
white-space: nowrap;
display: flex;
justify-content: start;">${Math.round(value)}</div>`
}
liveTable = observeTable(table)
function makeTableEditable(table, options) {
const defaults = {headerEditable: false, appendRows: true};
options = options === undefined ? {} : options;
for (let key in defaults) {
options[key] = options[key] === undefined ? defaults[key] : options[key];
}
return Generators.observe((_notify) => {
const navigate = (event) => {
const cell = event.target;
const row = cell.closest('tr');
const table = row.closest('table');
const isBody = row.parentNode.tagName === 'TBODY';
const isHeader = row.parentNode.tagName === 'THEAD';
const colIndex = cell.cellIndex;
const colCount = row.cells.length;
const rowIndex = row.rowIndex;
const rowCount = table.rows.length;
const headStop = options.headerEditable ? 0 : 1;
let direction = null;
let x = colIndex;
let y = rowIndex;
if (![
// https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes#heading-a-full-list-of-key-event-values
8, 9, 13, 16, 17, 18, 27, 33, 34, 35, 36, 37, 38, 39, 40, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 109, 189
].includes(event.which)) {
event.preventDefault();
}
else {
switch(event.code) {
// Tab cycles through the table, adding new rows as needed.
case 'Tab':
event.preventDefault();
if (event.altKey || event.shiftKey) {
direction = -1;
if (x - 1 < 0) {
if (y - 1 < headStop) break;
x = colCount - 1;
y = y - 1;
} else {
x = x - 1;
}
} else {
direction = 1;
if (x + 1 === colCount) {
x = 0;
y = y + 1;
} else {
x = x + 1;
}
}
break;
// Plain Enter navigates downwards.
// Shift + Enter or Alt + Enter goes up to the cell above.
case 'Enter':
event.preventDefault();
if (event.altKey || event.shiftKey) {
direction = -1;
x = x;
y = y - 1;
}
else {
direction = 1;
x = x;
y = y + 1;
}
break;
// The arrow keys allow you to navigate through cells.
// No new rows are added.
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
case 'Enter':
if (!event.altKey) break;
event.preventDefault();
switch(event.code) {
case 'ArrowUp':
direction = -1;
y = Math.max(y - 1, headStop);
break;
case 'ArrowDown':
direction = 1;
y = Math.min(y + 1, rowCount - 1);
break;
case 'ArrowLeft':
direction = -1;
x = Math.max(x - 1, 0);
break;
case 'ArrowRight':
direction = 1;
x = Math.min(x + 1, colCount - 1);
break;
}
break;
}
if (direction !== null) {
let nextRow;
if (y === rowCount) {
nextRow = options.appendRows ? addRowRelativeTo(row, direction) : row;
} else {
nextRow = table.rows[y];
}
let nextCell = nextRow.cells[x];
focusCell(nextCell);
}
};
}
table.addEventListener("keydown", navigate, false);
if (table.rows.length > 0) {
for (let row of table.rows) {
if (!options.headerEditable && row.rowIndex === 0) continue;
for (let cell of row.cells) {
if (cell.cellIndex === 0) continue;
let cellValue = cell.innerText
cell.innerHTML = `<div style="
width: ${Math.abs(cellValue) / (cell.cellIndex === 2 ? 2.5 : 10)}%;
float: left;
padding: 0px 0px 0px 2px;
text-indent: 2px;
box-sizing: border-box;
overflow: visible;
white-space: nowrap;
display: flex;
justify-content: start;">${cellValue}</div>`
if (cell.cellIndex === 3) continue;
cell.contentEditable = true;
}
}
}
return () => table.removeEventListener("keydown", navigate);
});
}
function observeTable(table) {
return Generators.observe((notify) => {
const keyinput = (event) => notify(parseTableData(table));
table.addEventListener("input", keyinput, false);
notify(parseTableData(table));
return () => window.removeEventListener("input", keyinput);
});
}
function parseTableData(table) {
const header = [];
const data = [];
for (let row of table.rows) {
const rowIndex = row.rowIndex;
const isHeader = row.parentNode.tagName === 'THEAD' && rowIndex === 0;
let obj = {};
for (let cell of row.cells) {
const head = header[cell.cellIndex];
if (isHeader) {
header.push(cell.innerText);
} else {
obj[head] = cell.innerText;
}
}
if (!isHeader) data.push(obj);
}
return JSON.parse(JSON.stringify(data));
}
function focusCell(td) {
const s = window.getSelection();
const r = document.createRange();
let textNode = td.childNodes[0];
const i = td.innerText.length;
td.focus();
if (textNode) {
r.setStart(textNode, i);
r.setEnd(textNode, i);
} else {
r.selectNode(td);
}
s.removeAllRanges();
s.addRange(r);
}
function addRowRelativeTo(tr, direction) {
const newTr = document.createElement('tr');
const insertPosition = direction == 1 ? 'afterend' : 'beforebegin';
tr.insertAdjacentElement(insertPosition, newTr);
for (let _td of Array.from(tr.children)) {
const newTd = document.createElement('td');
newTd.appendChild(document.createTextNode(''));
newTd.contentEditable = true;
newTr.appendChild(newTd);
}
return newTr;
}
// https://observablehq.com/@observablehq/text-color-annotations-in-markdown
rstbtn = d3.create('button').html('Reset').attr("id", "rstbtn").attr("class", "btn btn-quarto");
// https://observablehq.com/@recifs/add-a-class-to-an-observable-input--support
function labelToggle(inputType, inputLabel, inputValue, inputId) {
const input = inputType({label: inputLabel, value: inputValue});
input.setAttribute("id", inputId);
return input;
}
// https://observablehq.com/@observablehq/synchronized-inputs
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input", {bubbles: true}));
}
// https://observablehq.com/@observablehq/input-table
// https://stackoverflow.com/a/52079217
// Converts from degrees to radians.
function toRadians(degrees) { return degrees * Math.PI / 180; };
// Converts from radians to degrees.
function toDegrees(radians) { return radians * 180 / Math.PI; }
function coor2bear(strt, dest) {
const [strtLng, strtLat] = strt.map(toRadians);
const [destLng, destLat] = dest.map(toRadians);
return (toDegrees(Math.atan2(
Math.sin(destLng - strtLng) * Math.cos(destLat),
Math.cos(strtLat) * Math.sin(destLat) - Math.sin(strtLat) * Math.cos(destLat) * Math.cos(destLng - strtLng)
)) + 360) % 360;
}
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 2px",
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 turn2comp(turn) {
return ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.round(turn / 125) % 8]
}
function dec2rgb(d) {
const color = d3.color(piecewiseColor(d % 1))
return [color.r, color.g, color.b]
}
function dec2hue(d) {
return rgbToHsl(...dec2rgb(d))[0] * 1000
}
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
])
hueMtr = Math.round(colorD)
hueDeg = dec2hue(colorD / 1000) * .36
hStr = `hsl(${hueDeg}`
slStr = `, ${colorS / 10}%, ${colorL / 10}%)`
hslStr = hStr + slStr
bkgH = ({background: hStr + ", 100%, 50%)"})
bkgHsl = ({background: hslStr})
rainbowMtr = textcolor(hueMtr, bkgHsl)
rainbowDir = textcolor(turn2comp(hueMtr), bkgHsl)
rainbowDegC = textcolor(Math.round(colorD *.36), bkgHsl)
rainbowDegH = textcolor(Math.round(hueDeg), bkgHsl)
rainbowHex = textcolor(shortenHex(d3.color(hslStr).formatHex()).slice(1), bkgHsl)
rainbowN5zn = textcolor('-5', d3.color(`hsl(180${slStr}`).formatHex())
rainbowP583 = textcolor('5.83̅', d3.color(`hsl(129.88235294117646${slStr}`).formatHex())
// Show preview swatches of color
preview = () => {
const container = DOM.element('div')
d3.select(container).attr('style', 'display: flex;')
d3.select(container)
.append('div')
.text('Selected')
.style('font-weight', 'bold')
.append('div')
.classed('swatch', true)
.style('background-color', `hsl(${dec2hue(colorD / 1000) * .36}, ${colorS / 10}%, ${colorL / 10}%`);
d3.select(container)
.append('div')
.text('Preview')
.style('font-style', 'italic')
.append('div')
.classed('swatch', true)
.style('background-color', `rgb(${hoverRGB[0]}, ${hoverRGB[1]}, ${hoverRGB[2]}`)
d3.select(container).selectAll('div.swatch')
.style('width', '100px')
.style('height', '100px')
.style('margin-right', '8px')
.style('padding', '4px')
return container
}
// The currently hovered color
mutable hoverRGB = [255, 0, 0]
/**
* Draw an interactive color bar
* @param colorFn (t: number) => [number, number, number] Given a position on the bar (between 0 and 1), return its RGB
* @param onSelect (t: number) => void Callback for when a position is selected on the bar
*/
function colorbar({colorFn, onSelect}) {
const WIDTH = 360
const HEIGHT = 32
const container = DOM.element('div')
function handleSelect(coords) {
const t = coords[0] / WIDTH
onSelect(t)
}
let isDragging = false
const canvas = d3.select(container).append('canvas')
.attr('width', WIDTH)
.attr('height', HEIGHT)
.attr('style', 'cursor: crosshair; border: 1px solid black; border-radius: 2px;')
.on('mousedown', function() {
isDragging = true
handleSelect(d3.mouse(this))
})
.on('mouseup', () => { isDragging = false; })
.on('mousemove', function() {
const coords = d3.mouse(this)
if (isDragging) {
handleSelect(coords)
}
mutable hoverRGB = colorFn(coords[0] / WIDTH)
})
const ctx = canvas.node().getContext('2d')
const imgData = ctx.getImageData(0, 0, WIDTH, HEIGHT)
// Possible optimization: cache d3.range so we're not recalculating it a million times
d3.range(WIDTH).forEach(colIdx => {
const t = colIdx / WIDTH
const rgb = colorFn(t)
d3.range(HEIGHT).forEach(rowIdx => {
const screenIdx = rowIdx * WIDTH + colIdx
const imgDataIdx = 4 * screenIdx
imgData.data[imgDataIdx] = rgb[0]
imgData.data[imgDataIdx + 1] = rgb[1]
imgData.data[imgDataIdx + 2] = rgb[2]
imgData.data[imgDataIdx + 3] = 255
})
});
ctx.putImageData(imgData, 0, 0)
return container;
}
initialRGB = [255, 0, 0]
initialHSL = rgbToHsl(...initialRGB)
viewof colorR = Inputs.input(initialRGB[0])
viewof colorG = Inputs.input(initialRGB[1])
viewof colorB = Inputs.input(initialRGB[2])
viewof colorD = Inputs.input(dec2hue(initialHSL[0]))
viewof colorS = Inputs.input(1000)
viewof colorL = Inputs.input(500)
viewof colorA = Inputs.input(1000)
/**
* Update all color values based on current HSL
*/
onUpdateHSL = function(h, s, l) {
const rgb = hslToRgb(h / 1000, s / 1000, l / 1000)
console.log(h)
set(viewof colorR, rgb[0])
set(viewof colorG, rgb[1])
set(viewof colorB, rgb[2])
}
/**
* Credit to github.com/mjackson Source: https://gist.github.com/mjackson/5311256
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [ h, s, l ];
}
/**
* Credit to github.com/mjackson Source: https://gist.github.com/mjackson/5311256
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
*/
function hslToRgb(h, s, l){
let r, g, b;
if(s == 0){
r = g = b = l; // achromatic
} else {
let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
let p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
/**
* Credit github.com/mjackson. Source: https://gist.github.com/mjackson/5311256
*/
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
// https://observablehq.com/@maddievision/simple-canvas
pixelRatio = window.devicePixelRatio;
createCanvas = (width, height) => {
const canvas = document.createElement('canvas');
canvas.width = width * pixelRatio;
canvas.height = height * pixelRatio;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
return canvas
}
renderWithScale = (context, renderFunction) => {
context.save();
context.scale(pixelRatio, pixelRatio);
renderFunction()
context.restore();
}
quickRender = (width, height, renderer) => {
const canvas = createCanvas(width, height)
const context = canvas.getContext('2d')
renderWithScale(context, () => {
renderer(context)
})
return canvas
}
// http://howardhinnant.github.io/date_algorithms.html#civil_from_days
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 ?? (-Math.round(
(new Date).getTimezoneOffset() / 144) + 10) % 10
) / 10 + offset, zone]
}
octConnections = [
[0, 4],
[1, 5],
[2, 6],
[3, 7],
[4, 0],
[5, 1],
[6, 2],
[7, 3],
]
decConnections = [
[0, 5],
[1, 6],
[2, 7],
[3, 8],
[4, 9],
[5, 0],
[6, 1],
[7, 2],
[8, 3],
[9, 4]
]
hsl8 = [
`hsl(0, ${colorS / 10}%, ${colorL / 10}%)`, // 0
`hsl(44, ${colorS / 10}%, ${colorL / 10}%)`, // 875
`hsl(68, ${colorS / 10}%, ${colorL / 10}%)`, // 750
`hsl(96, ${colorS / 10}%, ${colorL / 10}%)`, // 625
`hsl(180, ${colorS / 10}%, ${colorL / 10}%)`, // 500
`hsl(216, ${colorS / 10}%, ${colorL / 10}%)`, // 375
`hsl(264, ${colorS / 10}%, ${colorL / 10}%)`, // 250
`hsl(292, ${colorS / 10}%, ${colorL / 10}%)`, // 125
]
hsl10 = [
`hsl(0, ${colorS / 10}%, ${colorL / 10}%)`, // red
`hsl(36, ${colorS / 10}%, ${colorL / 10}%)`, // orange
`hsl(60, ${colorS / 10}%, ${colorL / 10}%)`, // yellow
`hsl(80, ${colorS / 10}%, ${colorL / 10}%)`, // lime
`hsl(120, ${colorS / 10}%, ${colorL / 10}%)`, // green
`hsl(180, ${colorS / 10}%, ${colorL / 10}%)`, // cyan
`hsl(208, ${colorS / 10}%, ${colorL / 10}%)`, // azure
`hsl(240, ${colorS / 10}%, ${colorL / 10}%)`, // blue
`hsl(276, ${colorS / 10}%, ${colorL / 10}%)`, // violet
`hsl(300, ${colorS / 10}%, ${colorL / 10}%)`, // magenta
`hsl(0, ${colorS / 10}%, ${colorL / 10}%)`, // red
]
hsla10 = [
`hsla(0, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // red
`hsla(36, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // orange
`hsla(60, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // yellow
`hsla(80, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // lime
`hsla(120, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // green
`hsla(180, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // cyan
`hsla(208, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // azure
`hsla(240, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // blue
`hsla(276, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // violet
`hsla(300, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // magenta
`hsla(0, ${colorS / 10}%, ${colorL / 10}%, ${colorA / 10}%)`, // red
]
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]}
sunLonHsl = textcolor(sunLon, `hsl(${d3.hsl(piecewiseColor(sunLon % 1000 / 1000)).h}` + slStr)
sunLatHsl = textcolor(sunLat, `hsl(${d3.hsl(piecewiseColor((sunLat + 1000) % 1000 / 1000)).h}` + slStr)
dz = unix2dote(now)
ydz = dote2date(...dz)
decZone = ydz[2]
decZonePos = (decZone + 10) % 10
decSign = decZone < 0 ? "+" : "–"
ydzP0 = dote2date(...unix2dote(now, 0))
decYearP0 = ydzP0[0]
decYdaP0 = ydzP0[1]
decDateP0 = Math.floor(decYdaP0)
decDateP0pad = String(decDateP0).padStart(3, "0")
decTimeP0 = ydzP0[1] % 1
decDekP0 = Math.floor(decDateP0 / 10)
decDodP0 = decDateP0 % 10
decYearP0hsl0 = textcolor(decYearP0, `hsl(${d3.hsl(piecewiseColor(decYearP0 % 1000 / 1000)).h}` + slStr)
decYearP0hsl1 = textcolor(decYearP0, `hsl(${d3.hsl(piecewiseColor(decYearP0 % 1000 / 1000)).h}` + slStr)
decDateP0hsl0 = textcolor(decDateP0pad, `hsl(${d3.hsl(piecewiseColor(decDateP0 / (365 + isLeapP0))).h}` + slStr)
decDateP0hsl1 = textcolor(decDateP0pad, `hsl(${d3.hsl(piecewiseColor(decDateP0 / (365 + isLeapP0))).h}` + slStr)
decYdaP0hsl = textcolor(decYdaP0.toFixed(5).padStart(9, "0"), `hsl(${d3.hsl(piecewiseColor(decYdaP0 / (365 + isLeapP0))).h}` + slStr)
decTimeP0hsl0 = textcolor((decTimeP0 * 10).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeP0)).h}` + slStr)
decTimeP0hsl1 = textcolor((decTimeP0 * 10).toFixed(4), `hsl(${d3.hsl(piecewiseColor(decTimeP0)).h}` + slStr)
decDekP0hsl = textcolor(decDekP0, `hsl(${d3.hsl(piecewiseColor(decDekP0 / 37)).h}` + slStr)
decDodP0hsl = textcolor(decDodP0, `hsl(${d3.hsl(piecewiseColor(decDodP0 / 10)).h}` + slStr)
decLon = longitude % 10
decLonHsl = textcolor(parseFloat(decLon.toFixed(2)), `hsl(${d3.hsl(piecewiseColor(decLon / 10)).h}` + slStr)
decZon = Math.floor(decLon)
decZonHsl = textcolor(decZon, `hsl(${d3.hsl(piecewiseColor(decZon / 10)).h}` + slStr)
parLat = textcolor(parseFloat(latitude.toFixed(3)), `hsl(${d3.hsl(piecewiseColor((latitude + 1) % 1)).h}` + slStr)
parCos = Math.cos(latitude * 2 * Math.PI)
parLen = textcolor(parseFloat(parCos.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(parCos)).h}` + slStr)
conversionFactor = costype === "turns" ? "" : costype === "radians" ? tex`\,\tau\!` : tex`\times360`
zemsLeft = 1000 - 50 * Math.floor(now / 86400000 % 1 * 1000 % 1 * 100 % 21)
zLeft = textcolor(zemsLeft, `hsl(${d3.hsl(piecewiseColor(zemsLeft / 1000)).h}` + slStr)
point0long = long2turn(Place_A[0], 1)
point0zone = Math.floor(point0long)
point0lHsl = textcolor(parseFloat(point0long.toFixed(2)), `hsl(${d3.hsl(piecewiseColor(point0long / 10)).h}` + slStr)
point0zHsl = textcolor(point0zone, `hsl(${d3.hsl(piecewiseColor(point0zone / 10)).h}` + slStr)
isLeapP0 = decYearP0 % 4 == 0 && decYearP0 % 100 != 0 || decYearP0 % 400 == 0;
timezones = FileAttachment("../asset/timezones.json").json()
zones = topojson.feature(timezones, timezones.objects.timezones).features
mesh = topojson.mesh(timezones, timezones.objects.timezones)
color = d3.scaleSequential(d3.interpolateRdBu).domain([-12, 14])
coor = [[[-18, -89.98], [-18, 89.98], [18, 89.98], [18, -89.98], [-18, -89.98], ]]
deczones = [...Array(10).keys()].map(
i => ({
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [coor[0].map(t => [t[0]+36*i, t[1]])]
},
"properties": []
})
)
// https://observablehq.com/@enjalot/draggable-world-map-coordinates-input
// https://observablehq.com/@christophe-yamahata/great-circle-shortest-distance-between-two-locations-on-ea
function worldMapCoordinates(config = {}, dimensions) {
var n_point;
var lonA, lonB, latA, latB;
const {
value = [], title, description, width = dimensions[0]
} = Array.isArray(config) ? {value: config} : config;
const height = dimensions[1];
[lonA, latA] = value[0];
[lonB, latB] = value[1];
lonA = lonA != null ? lonA : 90;
latA = latA != null ? latA : 0.025;
lonB = lonB != null ? lonB : -90;
latB = latB != null ? latB : 36;
const formEl = html`<form style="width: ${width}px;"></form>`;
const context = DOM.context2d(width, height);
const canvas = context.canvas;
const projection = config[2]
.precision(0.1)
.fitSize([width, height], { type: "Sphere" });
const path = d3.geoPath(projection, context).pointRadius(2.5);
formEl.append(canvas);
function fillMesh(f) {
context.beginPath();
path(f);
context.fillStyle = color(f.properties.zone);
context.fill();
context.innerHTML = `<title>${f.properties.places} ${f.properties.time_zone}</title>`;
}
function draw(lon0, lat0, lon1, lat1) {
if (!utctoggle) {
context.beginPath(); path({type: "Sphere"});
context.fillStyle = window.darkmode ? "#007FFF" : mapcolors.ocean;
context.fill();
if (gridtoggle) {
deczones.map((f, i) => {
context.beginPath();
path(f);
context.fillStyle = hsla10[i];
context.fill();
})
}
}
if (utctoggle) {
zones.map(f => fillMesh(f))
}
context.beginPath();
path(land);
if (!utctoggle) {
context.fillStyle = window.darkmode ? "#0808" : mapcolors.land;
context.fill();
}
context.strokeStyle = `#000`;
context.stroke();
if (bordertoggle) {
context.beginPath();
path(borders);
context.lineWidth = 1.25;
context.strokeStyle = window.darkmode ? "#aaa" : "#333";
context.stroke();
}
if (utctoggle) {
context.beginPath();
path(mesh);
context.lineWidth = 1.25;
context.strokeStyle = `#999`;
context.stroke();
}
if (gridtoggle) {
context.beginPath();
path(graticule);
context.lineWidth = 1.25;
context.strokeStyle = utctoggle || !window.darkmode ? "#000" : "#fff";
context.stroke();
context.fillStyle = "#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, 54.7])));
d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, -59.7])));
// context.font = width < 760 ? "12px serif" : "21px serif";
// context.fillStyle = `#000`;
// d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, 27.5])));
// d3.range(-1.5, 342 + 1, 36).map(x => context.fillText(long2zone(x), ...projection([x, -48])));
// d3.range(-18, 336 + 1, 36).map(x => context.fillText(formatLongitude(x), ...projection([x, 90])));
// d3.range(-18, 336 + 1, 36).map(x => context.fillText(formatLongitude(x), ...projection([x, -90])));
}
if (suntoggle) {
context.beginPath();
path(night);
context.fillStyle = "rgba(0,0,255,0.3)";
context.fill();
context.beginPath();
path.pointRadius(width / 84 + 5);
path({type: "Point", coordinates: sun});
context.strokeStyle = "#0009";
context.fillStyle = "#ff0b";
context.lineWidth = 1;
context.stroke();
context.fill();
}
if (lon0 != null && lat0 != null) {
const pointPath = { type: "MultiPoint", coordinates: [[lon0, lat0]], id: "point0test"};
context.beginPath();
path.pointRadius(point_radius_2);
path(pointPath);
context.fillStyle = window.darkmode ? "#A24" : "#FDF";
context.fill();
context.strokeStyle = window.darkmode ? "white" : "black";
context.stroke();
}
if (lon1 != null && lat1 != null) {
const pointPath = { type: "MultiPoint", coordinates: [[lon1, lat1]] };
context.beginPath();
path.pointRadius(point_radius_2);
path(pointPath);
context.fillStyle = window.darkmode ? "#24B" : "#BFF";
context.fill();
context.strokeStyle = window.darkmode ? "white" : "black";
context.stroke();
}
// We draw the path between 2 points
var interpolation = d3.geoInterpolate([lon0,lat0],[lon1,lat1]);
var nb_points = d3.geoDistance([lon0,lat0],[lon1,lat1])*20;
for(let i = 1; i<nb_points; i++) {
const pointPath = { type: "MultiPoint", coordinates: [interpolation(i/nb_points)] };
path.pointRadius(point_radius);
context.beginPath(),
context.fillStyle = window.darkmode ? "#FF420E" : "orange",
path(pointPath),
context.strokeStyle = window.darkmode ? "white" : "black";
context.fill(),
context.stroke();
}
}
draw(lonA, latA, lonB, latB);
canvas.onclick = function(ev) {
const { offsetX, offsetY } = ev;
var coords = projection.invert([offsetX, offsetY]);
if(n_point==0){
lonA = +coords[0].toFixed(2);
latA = +coords[1].toFixed(2);
n_point = 1;
}else{
lonB = +coords[0].toFixed(2);
latB = +coords[1].toFixed(2);
n_point = 0;
}
const point0bear = Math.round(lati2turn(coor2bear([lonA, latA], [lonB, latB])))
set(viewof colorD, point0bear)
table.rows[1].cells[1].innerHTML = createCellDiv(long2turn(lonA), 10)
table.rows[2].cells[1].innerHTML = createCellDiv(long2turn(lonB), 10)
table.rows[1].cells[2].innerHTML = createCellDiv(lati2turn(latA), 2.5)
table.rows[2].cells[2].innerHTML = createCellDiv(lati2turn(latB), 2.5)
table.rows[1].cells[3].innerHTML = createCellDiv(point0bear, 10)
table.rows[2].cells[3].innerHTML = createCellDiv(lati2turn(coor2bear([lonB, latB], [lonA, latA])), 10)
draw(lonA, latA, lonB, latB);
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};
function resetlatlon() {
lonA = -90;
latA = 0;
lonB = -90;
latB = 36;
set(viewof bordertoggle, false);
set(viewof gridtoggle, false);
set(viewof suntoggle, false);
set(viewof utctoggle, false);
set(viewof yaw, 500);
set(viewof pitch, 0);
set(viewof roll, 0);
set(viewof select, projections.find(t => t.name === "Equirectangular (plate carrée)"));
set(viewof colorD, 0)
set(viewof colorS, 1000)
set(viewof colorL, 500)
table.rows[1].cells[1].innerHTML = createCellDiv(800, 10)
table.rows[2].cells[1].innerHTML = createCellDiv(800, 10)
table.rows[1].cells[2].innerHTML = createCellDiv(0, 2.5)
table.rows[2].cells[2].innerHTML = createCellDiv(100, 2.5)
table.rows[1].cells[3].innerHTML = createCellDiv(0, 10)
table.rows[2].cells[3].innerHTML = createCellDiv(500, 10)
draw(lonA, latA, lonB, latB);
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}
table.onkeyup = function(ev) {
if ([
// https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes#heading-a-full-list-of-key-event-values
9, 13, 27
].includes(ev.which)) {
lonA = turn2long(liveTable[0].Milliparallel);
latA = turn2degr(liveTable[0].Millimeridian);
lonB = turn2long(liveTable[1].Milliparallel);
latB = turn2degr(liveTable[1].Millimeridian);
const point0bear = Math.round(lati2turn(coor2bear([lonA, latA], [lonB, latB])))
set(viewof colorD, point0bear)
table.rows[1].cells[1].innerHTML = createCellDiv(long2turn(lonA), 10)
table.rows[2].cells[1].innerHTML = createCellDiv(long2turn(lonB), 10)
table.rows[1].cells[2].innerHTML = createCellDiv(lati2turn(latA), 2.5)
table.rows[2].cells[2].innerHTML = createCellDiv(lati2turn(latB), 2.5)
table.rows[1].cells[3].innerHTML = createCellDiv(point0bear, 10)
table.rows[2].cells[3].innerHTML = createCellDiv(lati2turn(coor2bear([lonB, latB], [lonA, latA])), 10)
draw(lonA, latA, lonB, latB);
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
} else if ([
// https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes#heading-a-full-list-of-key-event-values
8, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 109, 189
].includes(ev.which)) {
lonA = turn2long(liveTable[0].Milliparallel);
latA = turn2degr(liveTable[0].Millimeridian);
lonB = turn2long(liveTable[1].Milliparallel);
latB = turn2degr(liveTable[1].Millimeridian);
const point0bear = Math.round(lati2turn(coor2bear([lonA, latA], [lonB, latB])))
set(viewof colorD, point0bear)
table.rows[1].cells[3].innerHTML = createCellDiv(point0bear, 10)
table.rows[2].cells[3].innerHTML = createCellDiv(lati2turn(coor2bear([lonB, latB], [lonA, latA])), 10)
draw(lonA, latA, lonB, latB);
canvas.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}
}
rstbtn.on('click', resetlatlon);
document.getElementsByClassName("quarto-color-scheme-toggle")[0].onclick = function (e) {
window.quartoToggleColorScheme();
window.darkmode = document.getElementsByTagName("body")[0].className.match(/quarto-dark/) ? true : false;
draw(lonA, latA, lonB, latB);
return false;
};
const form = input({
type: "worldMapCoordinates",
title,
description,
display: v => "",
// html`<div style="width: ${width}px; white-space: nowrap; color: #444; text-align: center; font: 13px sans-serif; margin-bottom: 5px;">
// <span style="color: ${color_A}">Longitude: ${lonA != null ? lonA.toFixed(2) : ""}</span>
//
// <span style="color: ${color_A}">Latitude: ${latA != null ? latA.toFixed(2) : ""} </span>
// </div>
// <div style="width: ${width}px; white-space: nowrap; color: #444; text-align: center; font: 13px sans-serif; margin-bottom: 5px;">
// <span style="color: ${color_B}">Longitude: ${lonB != null ? lonB.toFixed(2) : ""}</span>
//
// <span style="color: ${color_B}">Latitude: ${latB != null ? latB.toFixed(2) : ""}</span>
// </div>`,
getValue: () => [[lonA != null ? lonA : null, latA != null ? latA : null], [lonB != null ? lonB : null, latB != null ? latB : null]],
form: formEl
});
return form;
}
point_radius = width / 900 * mapsize / 100 + 3
point_radius_2 = width / 150 * mapsize / 100 + 3
Place_A = coordinates[0]
Place_B = coordinates[1]
distance_km = (d3.geoDistance(Place_A, Place_B)* 6371).toFixed(0)
distance_mc = distance_km / 40
distance_mcHsl0 = textcolor(parseFloat(distance_mc.toFixed(0)), `hsl(${d3.hsl(piecewiseColor(distance_mc % 1000 / 1000)).h}` + slStr)
distance_mcHsl1 = textcolor(parseFloat(distance_mc.toFixed(0)), `hsl(${d3.hsl(piecewiseColor(distance_mc % 1000 / 1000)).h}` + slStr)
distance_c = distance_mc / 1000
distance_cHsl = textcolor(parseFloat(distance_c.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(distance_c % 1)).h}` + slStr)
velocity_v = travelspeed / 1000
velocity_vHsl0 = textcolor(parseFloat(velocity_v.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(velocity_v)).h}` + slStr)
velocity_vHsl1 = textcolor(parseFloat(velocity_v.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(velocity_v)).h}` + slStr)
velocity_mvHsl = textcolor(parseFloat(travelspeed.toFixed(0)), `hsl(${d3.hsl(piecewiseColor(travelspeed / 1000)).h}` + slStr)
traveltime = Math.round(distance_mc) / Math.round(travelspeed)
traveltimeHsl0 = Number.isFinite(traveltime) ? textcolor(parseFloat(Math.round(traveltime * 1000).toFixed(3)), `hsl(${d3.hsl(piecewiseColor(traveltime % 1)).h}` + slStr) : traveltime
traveltimeHsl1 = Number.isFinite(traveltime) ? textcolor(parseFloat(traveltime.toFixed(3)), `hsl(${d3.hsl(piecewiseColor(traveltime % 1)).h}` + slStr) : traveltime
nb_points = Math.round(distance_km/150)
d3format = require("d3-format@1")
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.5em;"></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;
}
// https://observablehq.com/@fil/d3-projections
projections = [
{ name: "Airocean", value: d3.geoAirocean },
{ name: "Airy’s minimum error", value: d3.geoAiry },
{ name: "Aitoff", value: d3.geoAitoff },
{ name: "American polyconic", value: d3.geoPolyconic },
{ name: "Armadillo", value: d3.geoArmadillo, options: { clip: { type: "Sphere" } } },
{ name: "August", value: d3.geoAugust },
{ name: "azimuthal equal-area", value: d3.geoAzimuthalEqualArea },
{ name: "azimuthal equidistant", value: d3.geoAzimuthalEquidistant },
{ name: "Baker dinomic", value: d3.geoBaker },
{ name: "Berghaus’ star", value: d3.geoBerghaus, options: { clip: { type: "Sphere" } } },
{ name: "Bertin’s 1953", value: d3.geoBertin1953 },
{ name: "Boggs’ eumorphic", value: d3.geoBoggs },
{ name: "Boggs’ eumorphic (interrupted)", value: d3.geoInterruptedBoggs, options: { clip: { type: "Sphere" } } },
{ name: "Bonne", value: d3.geoBonne },
{ name: "Bottomley", value: d3.geoBottomley },
{ name: "Bromley", value: d3.geoBromley },
{ name: "Butterfly (gnomonic)", value: d3.geoPolyhedralButterfly },
{ name: "Butterfly (Collignon)", value: d3.geoPolyhedralCollignon },
{ name: "Butterfly (Waterman)", value: d3.geoPolyhedralWaterman },
{ name: "Cahill-Keyes", value: d3.geoCahillKeyes },
{ name: "Collignon", value: d3.geoCollignon },
{ name: "conic equal-area", value: d3.geoConicEqualArea },
{ name: "conic equidistant", value: d3.geoConicEquidistant },
{ name: "Craig retroazimuthal", value: d3.geoCraig },
{ name: "Craster parabolic", value: d3.geoCraster },
{ name: "Cox", value: d3.geoCox },
{ name: "cubic", value: d3.geoCubic },
{ name: "cylindrical equal-area", value: d3.geoCylindricalEqualArea },
{ name: "cylindrical stereographic", value: d3.geoCylindricalStereographic },
{ name: "dodecahedral", value: d3.geoDodecahedral },
{ name: "Eckert I", value: d3.geoEckert1 },
{ name: "Eckert II", value: d3.geoEckert2 },
{ name: "Eckert III", value: d3.geoEckert3 },
{ name: "Eckert IV", value: d3.geoEckert4 },
{ name: "Eckert V", value: d3.geoEckert5 },
{ name: "Eckert VI", value: d3.geoEckert6 },
{ name: "Eisenlohr conformal", value: d3.geoEisenlohr },
{ name: "Equal Earth", value: d3.geoEqualEarth },
{ name: "Equirectangular (plate carrée)", value: d3.geoEquirectangular },
{ name: "Fahey pseudocylindrical", value: d3.geoFahey },
{ name: "flat-polar parabolic", value: d3.geoMtFlatPolarParabolic },
{ name: "flat-polar quartic", value: d3.geoMtFlatPolarQuartic },
{ name: "flat-polar sinusoidal", value: d3.geoMtFlatPolarSinusoidal },
{ name: "Foucaut’s stereographic equivalent", value: d3.geoFoucaut },
{ name: "Foucaut’s sinusoidal", value: d3.geoFoucautSinusoidal },
{ name: "general perspective", value: d3.geoSatellite },
{ name: "Gingery", value: d3.geoGingery, options: { clip: { type: "Sphere" } } },
{ name: "Ginzburg V", value: d3.geoGinzburg5 },
{ name: "Ginzburg VI", value: d3.geoGinzburg6 },
{ name: "Ginzburg VIII", value: d3.geoGinzburg8 },
{ name: "Ginzburg IX", value: d3.geoGinzburg9 },
{ name: "Goode’s homolosine", value: d3.geoHomolosine},
{ name: "Goode’s homolosine (interrupted)", value: d3.geoInterruptedHomolosine, options: { clip: { type: "Sphere" } } },
{ name: "gnomonic", value: d3.geoGnomonic },
{ name: "Gringorten square", value: d3.geoGringorten },
{ name: "Gringorten quincuncial", value: d3.geoGringortenQuincuncial },
{ name: "Guyou square", value: d3.geoGuyou },
{ name: "Hammer", value: d3.geoHammer },
{ name: "Hammer retroazimuthal", value: d3.geoHammerRetroazimuthal, options: { clip: { type: "Sphere" } } },
{ name: "HEALPix", value: d3.geoHealpix, options: { clip: { type: "Sphere" } } },
{ name: "Hill eucyclic", value: d3.geoHill },
{ name: "Hufnagel pseudocylindrical", value: d3.geoHufnagel },
{ name: "icosahedral", value: d3.geoIcosahedral },
{ name: "Imago", value: d3.geoImago },
{ name: "Kavrayskiy VII", value: d3.geoKavrayskiy7 },
{ name: "Lagrange conformal", value: d3.geoLagrange },
{ name: "Larrivée", value: d3.geoLarrivee },
{ name: "Laskowski tri-optimal", value: d3.geoLaskowski },
{ name: "Loximuthal", value: d3.geoLoximuthal },
{ name: "Mercator", value: d3.geoMercator },
{ name: "Miller cylindrical", value: d3.geoMiller },
{ name: "Mollweide", value: d3.geoMollweide },
{ name: "Mollweide (Goode’s interrupted)", value: d3.geoInterruptedMollweide, options: { clip: { type: "Sphere" } } },
{ name: "Mollweide (interrupted hemispheres)", value: d3.geoInterruptedMollweideHemispheres, options: { clip: { type: "Sphere" } } },
{ name: "Natural Earth", value: d3.geoNaturalEarth1 },
{ name: "Natural Earth II", value: d3.geoNaturalEarth2 },
{ name: "Nell–Hammer", value: d3.geoNellHammer },
{ name: "Nicolosi globular", value: d3.geoNicolosi },
{ name: "orthographic", value: d3.geoOrthographic },
{ name: "Patterson cylindrical", value: d3.geoPatterson },
{ name: "Peirce quincuncial", value: d3.geoPeirceQuincuncial },
{ name: "rectangular polyconic", value: d3.geoRectangularPolyconic },
{ name: "Robinson", value: d3.geoRobinson },
{ name: "sinusoidal", value: d3.geoSinusoidal },
{ name: "sinusoidal (interrupted)", value: d3.geoInterruptedSinusoidal, options: { clip: { type: "Sphere" } } },
{ name: "sinu-Mollweide", value: d3.geoSinuMollweide },
{ name: "sinu-Mollweide (interrupted)", value: d3.geoInterruptedSinuMollweide, options: { clip: { type: "Sphere" } } },
{ name: "stereographic", value: d3.geoStereographic },
{ name: "Lee’s tetrahedal", value: d3.geoTetrahedralLee },
{ name: "Times", value: d3.geoTimes },
{ name: "Tobler hyperelliptical", value: d3.geoHyperelliptical },
{ name: "transverse Mercator", value: d3.geoTransverseMercator },
{ name: "Van der Grinten", value: d3.geoVanDerGrinten },
{ name: "Van der Grinten II", value: d3.geoVanDerGrinten2 },
{ name: "Van der Grinten III", value: d3.geoVanDerGrinten3 },
{ name: "Van der Grinten IV", value: d3.geoVanDerGrinten4 },
{ name: "Wagner IV", value: d3.geoWagner4 },
{ name: "Wagner VI", value: d3.geoWagner6 },
{ name: "Wagner VII", value: d3.geoWagner7 },
{ name: "Werner", value: d3.geoBonne ? () => d3.geoBonne().parallel(90) : null },
{ name: "Wiechel", value: d3.geoWiechel },
{ name: "Winkel tripel", value: d3.geoWinkel3 }
]
mapcolors = ({
night: "#719fb6",
day: "#ffe438",
grid: "#4b6a79",
ocean: "#adeeff",
land: "#90ff7888",
sun: "#ffb438"
})
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;
}
function turn2degr(turns = -500, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return turns % 10**e * (360 / 10**e)
}
function turn2long(turns = -500, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return turns % 10**e * (360 / 10**e) - 18
}
function long2zone(degrees = -180) { return Math.floor(long2turn(degrees, 1)); }
function lati2turn(degrees = -180, e = 3) {
// turns: e=0, deciturns: e=1, etc.
return (degrees %= 360) / (360 / 10**e) % 10**e;
}
selectedProjection = select ? select.value() : d3.geoEquirectangular()
projection = {
let proj = selectedProjection;
if (proj.rotate) proj.rotate([-turn2long(yaw), -turn2degr(pitch), turn2degr(roll)]);
return proj;
}
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)];
}
sunLon = Math.round(long2turn(sun[0]))
sunLat = Math.round(lati2turn(sun[1]))
night = d3.geoCircle().radius(90).center(antipode(sun))()
antipode = ([longitude, latitude]) => [longitude + 180, -latitude]
height = {
const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, sphere)).bounds(sphere);
const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
return dy;
}
d3 = require("d3@5", "d3-array@3", "d3-geo@3", "d3-geo-projection@4", "d3-geo-polygon@1.8")
sphere = ({type: "Sphere"})
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))
)land = topojson.feature(world, world.objects.land)
world = fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/land-50m.json").then(response => response.json())
topojson = require("topojson-client@3")
solar = require("solar-calculator@0.3/dist/solar-calculator.min.js")
borders = topojson.mesh(countries, countries.objects.countries, (a, b) => a !== b)
countries = fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json").then(response => response.json())
deccolors = [
`hsl(20${slStr}`,
`hsl(24${slStr}`,
`hsl(28${slStr}`,
`hsl(36${slStr}`,
`hsl(44${slStr}`,
`hsl(48${slStr}`,
`hsl(52${slStr}`,
`hsl(60${slStr}`,
`hsl(64${slStr}`,
`hsl(68${slStr}`,
`hsl(72${slStr}`,
`hsl(80${slStr}`,
`hsl(88${slStr}`,
`hsl(92${slStr}`,
`hsl(96${slStr}`,
`hsl(120${slStr}`,
`hsl(148${slStr}`,
`hsl(156${slStr}`,
`hsl(164${slStr}`,
`hsl(180${slStr}`,
`hsl(192${slStr}`,
`hsl(196${slStr}`,
`hsl(200${slStr}`,
`hsl(208${slStr}`,
`hsl(216${slStr}`,
`hsl(220${slStr}`,
`hsl(224${slStr}`,
`hsl(240${slStr}`,
`hsl(260${slStr}`,
`hsl(264${slStr}`,
`hsl(268${slStr}`,
`hsl(276${slStr}`,
`hsl(284${slStr}`,
`hsl(288${slStr}`,
`hsl(292${slStr}`,
`hsl(300${slStr}`,
`hsl(320${slStr}`,
`hsl(328${slStr}`,
`hsl(336${slStr}`,
`hsl(0${slStr}`,
]
viewof size = Inputs.range([50, 700], {
value: 300,
step: 20,
label: 'size'
})
viewof numMajorTicks = Inputs.range([0, 45], {
value: 6,
step: 2,
label: "Major ticks"
})
viewof numMinorTicks = Inputs.range([0, 10], {
value: 2,
step: 1,
label: "Minor ticks"
})
function repeat(component, N, initialAngle=0) {
// NOTE: if component is a function, it will be called with (angle, i)
if (N <= 0) return "";
let result = [];
for (let i = 0; i < N; i++) {
let angle = (360 / N) * i + initialAngle;
let el = typeof component === 'function'? component(angle, i) : component;
result.push(`<g transform="rotate(${angle})">${el}</g>`);
}
return result.join("");
}
function tick(radius, length, color='black') {
return `<path d="M 0,${-radius} l 0,${-length}" fill="none" stroke="${color}" stroke-width="1" />`;
}
function directionMarker(radius, fontSize) { return (angle, _) => {
let label = {0: 'N', 45: 'NE', 90: 'E', 135: 'SE', 180: 'S', 225: 'SW', 270: 'W', 315: 'NW'}[angle] ?? '??';
return `<text y="${-radius-(margin/2)}" font-size="${fontSize}" text-anchor="middle" dy=".36em">${label}</text>`;
};
}
function turnMarker(radius, fontSize) { return (angle, _) => {
let label = {0: '0', 45: '125', 90: '250', 135: '375', 180: '500', 225: '625', 270: '750', 315: '875'}[angle] ?? '??';
return `<text y="${-radius-(margin/2)}" font-size="${fontSize}" text-anchor="middle" dy="-0.36em">${label}</text>`;
};
}
function pie(radius, width, narrowness=1.0, piecolors) {
return (_, i) => `<path id="piepath" d="M 0,0 L ${-width},${-radius} A ${width} ${width/2} 0 0 1 ${width} ${-radius} z" fill="${piecolors[i]}" stroke="black" stroke-width="0.5"/>`;
}
margin = size / 14
padding = 42
radius = size / 2 - margin - padding
window.darkmode = document.getElementsByTagName("body")[0].className.match(/quarto-dark/) ? true : false;function displayPalette(palette, { darkMode = false } = {}) {
return htl.html`
<div style="display: flex; flex-direction: column;">
<div style="margin-bottom:8px;">${cielabScatter(palette, { darkMode: darkMode })}</div>
<div style="display: flex; flex-direction: row; align-items: center; justify-content: space-evenly;">
<div style="">${lightnessSpectrum(palette, { darkMode: darkMode })}</div>
<div>${swatches(palette)}</div>
</div></div>
</div>`
}
paletteDots = function(palette, { darkMode = false } = {}) {
return Plot.plot( {
marks: [
Plot.dot(palette, {
x: (d,i) => i * 36,
r: 16,
fill: { value: (d) => d, label: "Color" },
stroke: darkMode ? "white" : "#202020" ,
tip: {
format: { x: false },
fill: darkMode ? "#202020" : "white"
}
}),
Plot.text(palette.map((d,i) => i), {
x: (d) => d * 36,
dx: 18,
dy: 16,
opacity: 0.8
}),
],
x: { domain: [ 0, 320 ], ticks: 0 },
height: 48,
width: (palette.length) * 40,
marginTop: 16,
style: { fontSize: 18, overflow: "visible" }
})
}
function cielabScatter(palette, { darkMode = false } = {}) {
let labPalette = palette.map((c,i)=>({...d3.lab(c), i, color: c, ...({})}));
return Plot.plot({
width: colorsize,
height: colorsize,
style: { fontSize: 12 },
marks: [
Plot.frame({rx: size / 2, ry: size / 2, opacity: 0.2}),
Plot.gridX({ticks: 3, opacity: 0.2}),
Plot.gridY({ticks: 3, opacity: 0.2}),
Plot.ruleX([0], {opacity: 0.25}),
Plot.ruleY([0], {opacity: 0.25}),
Plot.dot(labPalette, {
x: "a", y: "b", r: 5,
fill: { value: (d) => d.color, label: "Color" },
channels: {
L: { value: "l", label: "L*" }
},
tip: {
format: { fill: (d,i) => `${i}` },
fill: "#202020",
}
}),
],
x: {
domain: [-80, 80],
ticks: 3,
tickSize: 0,
labelArrow: null,
labelAnchor: "center",
label: "a*"
},
y: {
domain: [-80, 80],
ticks: 3,
tickRotate: 0,
tickSize: 0,
labelArrow: null,
labelAnchor: "center",
label: "b*"
}
});
}
function lightnessSpectrum(palette, { darkMode = false } = {}) {
let labPalette = palette.map((c,i)=>({...d3.lab(c), i, color: c, ...({})}));
return Plot.plot({
height: colorsize,
width: 70,
style: { fontSize: 12, overflow: "visible" }, // let the tip overflow the rect of the plot
marks: [
Plot.tickY( labPalette, {
y: (d) => d.l,
stroke: { value: (d) => d.color, label: "Color" },
strokeWidth: 3,
tip: {
anchor: "right",
frameAnchor: "left",
format: {
fontSize: 12,
stroke: (d,i) => `${i}`,
a: true, b: true
},
fill: "#202020"
},
channels: {
a: { value: "a", label: "a*" },
b: { value: "b", label: "b*" }
},
})
],
y: {
domain: [30, 100],
grid: true,
tickSize: 0,
labelAnchor: "center",
label: "L*",
labelArrow: false
},
})
}
function swatches(palette) {
return Plot.plot({
height: colorsize,
width: 50,
x: {ticks: 0},
margin: 0,
marks: [
Plot.barX(
palette, {
y: (d,i) => i,
fill: (d) => d,
inset: -1
}
)
]
})}
colorsize = 210
kilograms = kilograins * 0.064
kgrains = parseFloat(kilograins.toFixed(2))
kgrams = parseFloat(kilograms.toFixed(2))
zem2 = parseFloat((zems**2).toFixed(2))
meter2 = parseFloat(((zems*.4)**2).toFixed(2))
bmi = parseFloat((kilograins / zems**2).toFixed(2))
bmim2 = parseFloat((kilograms / meter2).toFixed(2))
bmiStr = bmi < 46.25 ? "underweight" : bmi < 62.5 ? "normal" : bmi < 75 ? "overweight" : "obese"
// https://observablehq.com/@magfoto/wavelengths-and-spectral-colours
// takes wavelength in nm and returns an rgba value
function wavelengthToColor(wavelength) {
let R,
G,
B,
alpha,
colorSpace,
wl = wavelength,
gamma = 1;
if (wl >= 380 && wl < 440) {
R = -1 * (wl - 440) / (440 - 380);
G = 0;
B = 1;
} else if (wl >= 440 && wl < 490) {
R = 0;
G = (wl - 440) / (490 - 440);
B = 1;
} else if (wl >= 490 && wl < 510) {
R = 0;
G = 1;
B = -1 * (wl - 510) / (510 - 490);
} else if (wl >= 510 && wl < 580) {
R = (wl - 510) / (580 - 510);
G = 1;
B = 0;
} else if (wl >= 580 && wl < 645) {
R = 1;
G = -1 * (wl - 645) / (645 - 580);
B = 0.0;
} else if (wl >= 645 && wl <= 780) {
R = 1;
G = 0;
B = 0;
} else {
R = 0;
G = 0;
B = 0;
}
// intensty is lower at the edges of the visible spectrum.
if (wl > 780 || wl < 380) {
alpha = 0;
} else if (wl > 700) {
alpha = (780 - wl) / (780 - 700);
} else if (wl < 420) {
alpha = (wl - 380) / (420 - 380);
} else {
alpha = 1;
}
colorSpace = ["rgba(" + (R * 100) + "%," + (G * 100) + "%," + (B * 100) + "%, " + alpha + ")", R, G, B, alpha]
// colorSpace is an array with 5 elements.
// The first element is the complete code as a string.
// Use colorSpace[0] as is to display the desired color.
// Use the last four elements alone or together to access each of the individual r, g, and b channels.
return colorSpace;
}
// https://observablehq.com/@freedmand/sounds
function piano(stlibWidth) {
const width = 960;
const keyHeight = 450;
const height = 575;
const whiteKeys = 11;
const blackKeys = [1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1];
const whiteOffsets = blackKeys.reduce((x, y) => x.concat([y + x[x.length - 1] + 1]), [0]);
const svg = html`<svg width="100%" height="auto" viewBox="0 0 ${width} ${height}"
xmlns="http://www.w3.org/2000/svg"></svg>`;
function wrap(elem, note) {
const freq = 440 * Math.pow(2, note / 12);
// Play a note when clicked.
const oscillator = ctx.createOscillator();
const gain = ctx.createGain();
gain.gain.value = 0;
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(freq, ctx.currentTime);
oscillator.connect(gain);
gain.connect(ctx.destination);
oscillator.start();
elem.style.cursor = 'pointer';
elem.onclick = () => {
gain.gain.cancelScheduledValues(ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.1, ctx.currentTime + 0.05);
gain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.3);
};
return elem;
}
// Draw the white keys.
for (let i = 0; i <= whiteKeys - 1; i++) {
svg.appendChild(wrap(html`<svg><rect x="${width * i / whiteKeys}" y="0" width="${width / whiteKeys}" height="${keyHeight}" fill=${whiteKeyColors[i]} stroke="black" stroke-width="2"/></svg>`, whiteOffsets[i] - 4));
svg.appendChild(html`<svg><text style="user-select: none;" x="${width * (i + 0.5) / whiteKeys}" y="${keyHeight + 42}" font-family="monospace" id="pianotext" font-size="36" text-anchor="middle">${String.fromCharCode('A'.charCodeAt(0) + (i + 5) % 7) + (i < 4 ? "3" : "4")}</text></svg>`);
svg.appendChild(html`<svg><text style="user-select: none;" x="${width * (i + 0.5) / whiteKeys}" y="${keyHeight + 82}" font-family="monospace" id="pianotext" font-size="36" text-anchor="middle">${Math.round(220 * Math.pow(2, (whiteOffsets[i] - 4) / 12) * .864)}</text></svg>`);
svg.appendChild(html`<svg><text style="user-select: none;" x="${width * (i + 0.5) / whiteKeys}" y="${keyHeight + 122}" font-family="monospace" id="pianotext" font-size="36" text-anchor="middle">${Math.round(73504.8 / (220 * Math.pow(2, (whiteOffsets[i] - 4) / 12) * .864))}</text></svg>`);
}
// Draw the black keys.
for (let i = 0; i <= whiteKeys - 2; i++) {
if (blackKeys[i] == 1) {
svg.appendChild(wrap(html`<svg><rect x="${width * ((i + 0.65) / whiteKeys)}" y="0" width="${width / whiteKeys * 0.7}" height="${keyHeight * 0.55}" fill=${blackKeyColors[i]} stroke="black" stroke-width="2"/></svg>`, whiteOffsets[i] - 4 + blackKeys[i]));
}
}
return svg;
}
whiteKeyColors = [
"#fff",
"#fff",
"#fff",
xetHex[1],
xetHex[2],
xetHex[4],
xetHex[6],
xetHex[7],
xetHex[9],
xetHex[11],
"#fff",
"#fff",
"#fff",
]
blackKeyColors = [
"#000",
"#000",
xetHex[0],
"",
xetHex[3],
xetHex[5],
"",
xetHex[8],
xetHex[10],
"#000",
"#000",
"#000",
]
// https://observablehq.com/@freedmand/sounds
ctx = new (window.AudioContext || window.webkitAudioContext)()
function Play(genFn, duration, frequency) {
return new SoundBuffer(genFn, duration, frequency).gui();
}
class SoundBuffer {
constructor(genFn, duration, frequency) {
this.duration = duration;
this.frequency = frequency;
// Create an audio buffer.
this.audioBuffer = ctx.createBuffer(1, ctx.sampleRate * this.duration, ctx.sampleRate);
this.buffer = this.audioBuffer.getChannelData(0);
let max = 0;
for (let i = 0; i < this.audioBuffer.length; i++) {
const value = genFn(i / ctx.sampleRate);
this.buffer[i] = value;
if (Math.abs(value) > max) max = Math.abs(value);
}
for (let i = 0; i < this.audioBuffer.length; i++) {
this.buffer[i] = this.buffer[i] / max;
}
}
play(maxVol = 0.3) {
this.stop();
this.source = ctx.createBufferSource();
this.source.buffer = this.audioBuffer;
const gain = ctx.createGain();
gain.gain.value = maxVol;
this.source.connect(gain);
gain.connect(ctx.destination);
this.source.start();
}
stop() {
if (this.source) this.source.stop();
}
draw(height = 50, width = width, color = 'blue') {
const drawingCtx = DOM.context2d(width, height);
// Draw the middle line.
drawingCtx.strokeStyle = 'gainsboro';
drawingCtx.beginPath();
drawingCtx.moveTo(0, height / 2);
drawingCtx.lineTo(width, height / 2);
drawingCtx.stroke();
// Draw the waveform.
drawingCtx.strokeStyle = color;
drawingCtx.beginPath();
for (let i = 0; i < width; i++) {
const value = this.buffer[Math.floor(i / width * this.audioBuffer.length)];
const y = height - Math.floor((value / 2 + 0.5) * height * 0.9 + height * 0.05);
if (i == 0) {
drawingCtx.moveTo(i, y);
} else {
drawingCtx.lineTo(i, y);
}
}
drawingCtx.stroke();
return drawingCtx.canvas;
}
gui() {
const ui = html`<style>
.sound-player {
border: solid 1px gainsboro;
background: #f5f5f5;
font-family: sans-serif;
color: #6f6f6f;
font-size: 0.8em;
}
.sound-pane {
height: 50px;
background: white;
margin: 8px;
border: solid 1px gainsboro;
position: relative;
}
.icons {
margin: 0 8px 8px 8px;
}
.icons .button {
cursor: pointer;
border: solid 1px transparent;
}
.icons .button:hover {
border: solid 1px gainsboro;
}
.cursor {
background: red;
width: 2px;
height: 100%;
position: absolute;
}
</style>
<div class="sound-player">
<div class="sound-pane">
<span class="cursor" style="display: none;"></span>
</div>
<div class="icons">
<span class="button play-button" style="font-size:18px;">▶</span>
<span class="button stop-button" style="font-size:18px;">◼</span>
<span class="duration">Frequency: ${this.frequency} ib, Duration: ${Math.round(this.duration / .864)} b</span>
</div>
</div>`;
const cursor = ui.querySelector('.cursor');
let interval = null;
const resetInterval = () => {
if (interval != null) {
clearInterval(interval);
interval = null;
}
};
const soundPlayer = ui.querySelector('.sound-player');
ui.querySelector('.sound-pane').appendChild(this.draw(46, width - 20));
ui.querySelector('.play-button').onclick = () => {
cursor.style.left = '0';
this.play();
cursor.style.display = 'block';
const playTime = Date.now();
resetInterval();
interval = setInterval(() => {
if (!document.contains(soundPlayer)) {
resetInterval();
this.stop();
}
let progress = (Date.now() - playTime) / this.duration / 1000;
if (progress < 0) progress = 0;
if (progress > 1) {
progress = 1;
resetInterval();
this.stop();
cursor.style.display = 'none';
}
cursor.style.left = `${Math.floor(progress * (width - 20))}px`;
}, 20);
};
ui.querySelector('.stop-button').onclick = () => {
resetInterval();
this.stop();
cursor.style.display = 'none';
};
return ui;
}
}
function shortenHex(hex) {
if (!/^#([0-9a-f]{3}){1,2}$/i.test(hex)) {
return hex;
}
hex = hex.replace("#", "");
if (hex.length === 6 && hex[0] === hex[1] && hex[2] === hex[3] && hex[4] === hex[5]) {
return "#" + hex[0] + hex[2] + hex[4];
}
return "#" + hex;
}
piecewiseIob = d3.piecewise(d3.interpolateRound, [
301.734720,
319.675162,
338.684026,
358.823261,
380.160000,
402.765523,
426.715171,
452.088950,
478.971619,
507.452688,
537.627456,
569.596406,
603.466416,
639.351360,
])
piecewiseLen = d3.piecewise(d3.interpolateRound, [
2436.073648,
2299.359125,
2170.306080,
2048.495960,
1933.522727,
1825.002285,
1722.572924,
1625.892425,
1534.637900,
1448.505481,
1367.206961,
1290.471625,
1218.042928,
1149.677698,
])
class minMax {
constructor(limits) {
this.min = Math.min(...limits)
this.max = Math.max(...limits)
}
scale(val) {
return (val - this.min) / (this.max - this.min)
}
}
octave4scaler = new minMax([200, 400])
freqs = [
201.38,
213.36,
226.04,
239.49,
253.73,
268.81,
284.80,
301.73,
319.68,
338.68,
358.82,
380.16,
]
notes = [
"A♯",
"B",
"C",
"C♯",
"D",
"D♯",
"E",
"F",
"F#",
"G",
"G♯",
"A",
]
xet = freqs.map(x => octave4scaler.scale(x))
xetFix = xet.map(x => parseFloat((x * 10).toFixed(2)))
xetCol = xet.map(piecewiseColor)
xetHex = xetCol.map(x => d3.color(x).formatHex())
xetHue = xetCol.map(x => Math.round(d3.hsl(x).h))
xetIob = xet.map(piecewiseIob)
xetLen = xet.map(piecewiseLen)
hues = Object.fromEntries([
0.002,
0.00390625,
0.004,
0.0058,
0.0078125,
0.008,
0.014,
0.015625,
0.0158,
0.016,
0.021,
0.022,
0.024,
0.030,
0.03125,
0.040,
0.039,
0.0625,
0.065,
0.067,
0.130,
0.185,
1/3,
0.40069,
0.41302,
0.413,
0.4132,
0.420,
0.450,
0.460,
0.480,
0.490,
0.49008,
0.599,
0.704,
0.754,
0.788,
0.815,
0.864,
0.935,
0.960,
].map(i => [i, d3.hsl(piecewiseColor(i)).h])
);
h26div300 = d3.hsl(piecewiseColor(26 / 300)).h
hD039 = d3.hsl(piecewiseColor(39 / 365)).h
hD080 = d3.hsl(piecewiseColor(80 / 365)).h
hD285 = d3.hsl(piecewiseColor(285 / 365)).h
fMile = 1.6 / 1.609344
fInch = 25 / 25.4
hIob = d3.hsl(piecewiseColor(1 / .864 % 1)).h
hDrop = d3.hsl(piecewiseColor(64 / 51 % 1)).h
hMass = d3.hsl(piecewiseColor(448 / 453.59237)).h
hGall = d3.hsl(piecewiseColor(3.584 / 3.785411784)).h
hBarr = d3.hsl(piecewiseColor(128 / 119.24 % 1)).h
hCara = d3.hsl(piecewiseColor(192 / 200)).h
hAvOz = d3.hsl(piecewiseColor(32 / 28.349523125 % 1)).h
hFlOz = d3.hsl(piecewiseColor(32 / 29.5735295625 % 1)).h
hInch = d3.hsl(piecewiseColor(fInch)).h
hPerS = d3.hsl(piecewiseColor(fInch * .96)).h
hSqIn = d3.hsl(piecewiseColor(fInch**2)).h
hCuIn = d3.hsl(piecewiseColor(fInch**3)).h
hMile = d3.hsl(piecewiseColor(fMile)).h
hSqMi = d3.hsl(piecewiseColor(fMile**2)).h
bcHue = (xetHue[1] + xetHue[2]) / 2
ddsHue = (xetHue[4] + xetHue[5]) / 2
dseHue = (xetHue[5] + xetHue[6]) / 2
// https://observablehq.com/@mcmire/tone-map
axisColor = d3.hsl(0, 0, 0.5)
GraphableTone = {
const _assertRequiredKeys = Symbol("assertRequiredKeys");
class GraphableTone {
constructor(args) {
this[_assertRequiredKeys](args);
const { recognizedProps, unrecognizedProps } = lod.reduce(
args,
(results, value, key) => {
if (this.constructor.knownKeys.indexOf(key) !== -1) {
results.recognizedProps[key] = value;
} else {
results.unrecognizedProps[key] = value;
}
return results;
},
{ recognizedProps: {}, unrecognizedProps: {} }
);
this._setProps(recognizedProps, unrecognizedProps);
}
get origin() {
return this._origin;
}
set origin(origin) {
this._origin = Fraction(origin);
}
get frequency() {
return this.origin.mul(this.ratio);
}
cloneWith(overrideProps) {
const recognizedProps = lod.pick(this, this.constructor.knownKeys);
return new this.constructor({
...recognizedProps,
...this.unrecognizedProps,
...overrideProps
});
}
toPlainObject() {
const recognizedProps = lod.pick(this, this.constructor.knownKeys);
return {
...recognizedProps,
...this.unrecognizedProps
};
}
toDebug() {
return {
...this,
ratio: `${this.ratio.n}/${this.ratio.d}`,
frequency: this.frequency.valueOf(),
origin: this.origin.valueOf(),
seriesIndex: this.seriesIndex,
colorIndex: this.colorIndex,
index: this.index,
alwaysShowLabel: this.alwaysShowLabel
};
}
_setProps(recognizedProps, unrecognizedProps) {
this.ratio = recognizedProps.ratio;
this.origin = recognizedProps.origin != null ? recognizedProps.origin : originFrequency;
this.index = recognizedProps.index;
this.seriesIndex = recognizedProps.seriesIndex != null ? recognizedProps.seriesIndex : 0;
this.colorIndex = recognizedProps.colorIndex != null ? recognizedProps.colorIndex : this.seriesIndex;
this.alwaysShowLabel = recognizedProps.alwaysShowLabel != null ? recognizedProps.alwaysShowLabel : true;
Object.assign(this, unrecognizedProps);
this.unrecognizedProps = unrecognizedProps;
}
[_assertRequiredKeys](args) {
const missingKey = this.constructor.requiredKeys.find(requiredKey => {
return !args.hasOwnProperty(requiredKey);
});
if (missingKey != null) {
throw new Error(`Missing key: ${missingKey}`);
}
}
}
GraphableTone.knownKeys = [
"ratio",
"origin",
"index",
"seriesIndex",
"colorIndex",
"alwaysShowLabel"
];
GraphableTone.requiredKeys = ["ratio"];
GraphableTone.wrap = (value, options = {}) => {
const defaults = options.defaults || {};
if (value instanceof GraphableTone) {
return value;
} else {
return new GraphableTone({ ...defaults, ...value });
}
}
return GraphableTone;
}
renderGraph = (possibleTones, config = {}) => {
const tones = possibleTones.map((possibleTone, index) => {
return GraphableTone.wrap(possibleTone, { defaults: { index }});
});
const mapToY = mapToYFor(tones, config);
synth.releaseAll();
const height = graphHeight;
const svg = d3.select(DOM.svg(width, height))
.attr("viewBox", `6 -25 ${width} ${height}`)
.attr("preserveAspectRatio", "xMidYMid meet")
.style("width", "100%")
.style("height", "auto");
if (config.drawConnections) {
addConnectionsTo(svg, tones, config);
}
addStiltsTo(svg, tones, config);
const circles = addPointsTo(svg, tones, config);
addPointLabelsTo(svg, tones, config);
addXAxisTo(svg, tones, config);
// addYAxisTo(svg, tones, config);
augmentGraphToPlayTones(svg, circles, {
getTonesFrom: (tone) => [tone],
onMouseDown: (circle, tone) => {
circle.transition("mousedown")
.duration(transitionDuration)
.ease(d3.easeLinear)
.attr("r", pointRadius * 2);
svg.select(`.point-label--${tone.seriesIndex}--${tone.index}`)
.transition()
.attr("opacity", 1)
.attr("y", t => mapToY(t.label) - (pointRadius * 2) - 10);
},
onMouseUp: (circle, tone) => {
circle.transition("mouseup")
.duration(transitionDuration)
.ease(d3.easeLinear)
.attr("r", pointRadius);
svg.selectAll(".point-label")
.transition()
.attr("opacity", t => (t.alwaysShowLabel && !config.drawConnections) ? 1 : 0)
.attr("y", t => mapToY(t.label) - (pointRadius * 2));
}
});
return svg.node();
}
graphHeight = 400
augmentGraphToPlayTones = (
svg,
elements,
{
getTonesFrom,
getTargetFrom = (element) => element,
eventSource = "augmentGraphToPlayTones",
onMouseDown = () => {},
onMouseUp = () => {},
}
) => {
const BUFFER_TIME = 250; // ms
const notesBeingPlayed = [];
const interruptAllEventsFor = (selection) => {
selection
.interrupt("mouseover")
.interrupt("mouseout")
.interrupt("mousedown");
};
elements.each((datum, i, nodes) => {
const element = d3.select(nodes[i]);
const target = getTargetFrom(element);
target
.style("cursor", "pointer")
.on(`mousedown.${eventSource}`, () => {
interruptAllEventsFor(target);
getTonesFrom(datum).forEach(tone => {
synth.triggerAttack((tone.freq ?? tone.frequency.valueOf()));
notesBeingPlayed.push({
datum: datum,
target: target,
element: element,
tone: tone,
time: new Date()
});
});
onMouseDown(target, datum, element);
});
});
svg.on(`mouseup.${eventSource}`, () => {
const fn = (note) => {
interruptAllEventsFor(note.target);
synth.triggerRelease((note.tone.freq ?? note.tone.frequency.valueOf()));
onMouseUp(note.target, note.datum, note.element);
};
notesBeingPlayed.forEach(note => {
const now = new Date();
const timeBuffer = BUFFER_TIME - (now - note.time);
if (timeBuffer > 0) {
setTimeout(() => fn(note), timeBuffer);
} else {
fn(note);
}
});
// clear the whole thing
notesBeingPlayed.splice(0, notesBeingPlayed.length);
});
}
reduceFraction = (fraction, { min, max }) => {
let n = 0;
while (fraction.valueOf() < min || fraction.valueOf() > max) {
if (n > 100) {
throw "Too many iterations";
}
if (fraction > max) {
fraction = fraction.div(2);
} else if (fraction < min) {
fraction = fraction.mul(2);
}
n++;
}
return fraction;
}
primeNumbersUpTo = (limit) => {
const maxMultiple = Math.sqrt(limit);
const workingPrimes = lod.range(2, limit + 1).reduce((o, n) => ({ ...o, [n]: true }), {});
for (let multiple = 2; multiple <= maxMultiple; multiple++) {
if (workingPrimes[multiple]) {
for (let nonPrime = multiple * multiple; nonPrime <= limit; nonPrime += multiple) {
workingPrimes[nonPrime] = false;
}
}
}
const primes = lod.reduce(workingPrimes, (array, isPrime, n) => {
if (isPrime) {
return array.concat([
parseInt(n, 10)
]);
} else {
return array;
}
}, []);
return [1].concat(primes);
}
synth = new Tone.PolySynth(16, Tone.Synth, {
oscillator: {
type: "sine",
volume: -20
},
envelope: {
attack: 0.05,
decay: 0,
sustain: 1,
release: 1.2
}
}).toMaster()
origin = Fraction(originFrequency)
lollimargin = ({top: 36, right: width < 450 ? 10 : width < 500 ? 11 : width < 550 ? 12 : width < 600 ? 13 : width < 650 ? 14 : width < 700 ? 15 : 16, bottom: 28, left: width < 450 ? 10 : width < 500 ? 11 : width < 550 ? 12 : width < 600 ? 13 : width < 650 ? 14 : width < 700 ? 15 : 16})
numberOfColors = 10
rowHeight = 60
originFrequency = 440
pointRadius = width < 600 ? 10 : width < 650 ? 11 : width < 700 ? 12 : width < 750 ? 13 : width < 800 ? 14 : width < 850 ? 15 : width < 900 ? 16 : width < 950 ? 17 : 18
transitionDuration = 200
addStiltsTo = (svg, tones, config) => {
const mapToX = mapToXFor(tones, config);
const mapToY = mapToYFor(tones, config);
const height = calculateGraphHeight(tones);
svg
.append("g")
.attr("stroke", config.axisTextColor ?? axisColor)
.attr("stroke-dasharray", "2, 3")
.attr("stroke-width", 1)
.selectAll("line")
.data(tones)
.enter().append("line")
.attr("x1", tone => mapToX(tone.x ?? tone.frequency))
.attr("x2", tone => mapToX(tone.x ?? tone.frequency))
.attr("y1", tone => mapToY(tone.label))
.attr("y2", height - lollimargin.bottom);
}
addPointsTo = (svg, tones, config) => {
const mapToX = mapToXFor(tones, config);
const mapToY = mapToYFor(tones, config);
const colors = colorsFor(tones);
const circles = svg
.append("g")
.selectAll("circle")
.data(tones)
.enter().append("circle")
.attr("cx", t => mapToX(t.x ?? t.frequency))
.attr("cy", t => mapToY(t.label))
.attr("r", pointRadius)
.attr("fill", t => t.color ?? (t.isColored === false ? "#ccc" : colors[t.colorIndex]))
.attr("stroke", config.axisTextColor ?? axisColor)
.attr("stroke-width", 1);
return circles;
}
addPointLabelsTo = (svg, tones, config) => {
const mapToX = mapToXFor(tones, config);
const mapToY = mapToYFor(tones, config);
const colors = colorsFor(tones);
svg
.append("g")
.selectAll("text")
.data(tones)
.enter().append("text")
.attr("class", (tone, i) => {
return `point-label point-label--${tone.seriesIndex}--${tone.index}`;
})
.text(tone => {
if (tone.label != null) return tone.label;
const ratioAsString = `${tone.ratio.n}/${tone.ratio.d}`;
if (tone.frequency.equals(tone.origin)) {
return tone.origin;
} else {
return ratioAsString;
}
})
.attr("class", "unselectable")
.attr("font-family", "sans-serif")
.attr("font-size", width < 600 ? "32px" : width < 650 ? "34px" : width < 700 ? "36px" : width < 750 ? "38px" : width < 800 ? "40px" : width < 850 ? "42px" : width < 900 ? "44px" : width < 950 ? "46px" : "48px")
.attr("text-anchor", "middle")
.attr("fill", t => t.color ?? (t.isColored === false ? "#ccc" : colors[t.colorIndex]))
.attr("stroke", config.axisTextColor ?? axisColor)
.attr("stroke-width", width < 400 ? 1.25 : width < 500 ? 1.5 : width < 600 ? 1.75 : width < 700 ? 2 : width < 800 ? 2.25 : width < 900 ? 2.5 : width < 1000 ? 2.75 : 3)
.attr("paint-order", "stroke")
.attr("stroke-linejoin", "round")
.attr("x", t => mapToX(t.x ?? t.frequency))
.attr("y", t => mapToY(t.label) - (pointRadius * 2))
.attr("opacity", (t, _) => {
return (t.alwaysShowLabel && !config.drawConnections) ? 1 : 0;
});
}
addYAxisTo = (svg, tones, config) => {
const mapToY = mapToYFor(tones, config);
const axis = d3.axisLeft(mapToY);
svg.append("g")
.attr("transform", `translate(${lollimargin.left * 3},0)`)
.call(axis)
.selectAll("text")
.attr("class", "unselectable")
.attr("font-size", config.axisFontSize ?? "16px");
}
addXAxisTo = (svg, tones, config) => {
svg.append("g").call(g => {
const height = calculateGraphHeight(tones);
const axis = d3.axisBottom(mapToXFor(tones, config)).tickSizeOuter(0);
if (config.xAxis != null && config.xAxis.ticks) {
axis.tickValues(config.xAxis.ticks);
}
const axisGroup = g
.attr("transform", `translate(0,${height - lollimargin.bottom})`)
.call(axis);
axisGroup.selectAll("text")
.attr("class", "unselectable")
.attr("font-size", width < 600 ? "16px" : width < 650 ? "18px" : width < 700 ? "20px" : width < 750 ? "22px" : width < 800 ? "24px" : width < 850 ? "26px" : width < 900 ? "28px" : width < 950 ? "30px" : "32px")
.attr("fill", config.axisTextColor ?? axisColor);
axisGroup.selectAll("path")
.attr("stroke", config.axisTextColor ?? axisColor);
axisGroup.selectAll("line")
.attr("stroke", config.axisTextColor ?? axisColor);
});
svg.append("text")
.attr("class", "unselectable")
.attr("x", (width - lollimargin.left - lollimargin.right) / 2 + lollimargin.left)
.attr("y", calculateGraphHeight(tones) + (width < 600 ? 20 : width < 650 ? 22 : width < 700 ? 24 : width < 750 ? 26 : width < 800 ? 28 : width < 850 ? 30 : width < 900 ? 32 : width < 950 ? 34 : 36))
.attr("text-anchor", "middle")
.attr("font-size", config.axisTitleFontSize ?? ("font-size", width < 600 ? "20px" : width < 650 ? "22px" : width < 700 ? "24px" : width < 750 ? "26px" : width < 800 ? "28px" : width < 850 ? "30px" : width < 900 ? "32px" : width < 950 ? "34px" : "36px"))
.attr("fill", config.axisTextColor ?? axisColor)
.text(config.xAxisTitle ?? "Perbeat (inverse centimilliday)");
}
addConnectionsTo = (svg, tones, config) => {
const connections = ToneConnections.generate(tones, config);
const gradients = Object.values(
lod.keyBy(
connections.map(connection => connection.gradient),
gradient => gradient.id
)
);
const linearGradientGroup = svg.append("defs")
.selectAll("linearGradient")
.data(gradients)
.enter().append("linearGradient")
.attr("id", gradient => gradient.id);
linearGradientGroup.append("stop")
.attr("offset", "0%")
.attr("stop-color", gradient => gradient.from);
linearGradientGroup.append("stop")
.attr("offset", "100%")
.attr("stop-color", gradient => gradient.to);
svg.append("g")
.selectAll("path")
.data(connections)
.enter().append("path")
.attr("d", connection => {
const curve = connection.curve;
return [
`M ${curve.start.x} ${curve.start.y}`,
/*
`C ${curve.control1.x} ${curve.control1.y}`,
`${curve.control2.x} ${curve.control2.y}`,
*/
`L ${curve.end.x} ${curve.end.y + 0.1}`
].join(" ");
})
.attr("stroke-width", "1px")
.attr("stroke", connection => `url(#${connection.gradient.id})`)
.attr("fill", "none");
svg.append("g")
.selectAll("text")
.data(connections)
.enter().append("text")
.text(connection => connection.text.content)
.attr("class", "unselectable")
.attr("font-family", "sans-serif")
.attr("font-size", "13px")
.attr("font-weight", "bold")
.attr("text-anchor", "middle")
.attr("x", connection => connection.text.x)
.attr("y", connection => connection.text.y)
.attr("transform", connection => {
return `rotate(${connection.text.angle} ${connection.text.x} ${connection.text.y})`
})
.attr("fill", connection => connection.gradient.to)
}
ToneConnections = {
const gradients = {};
let lastGradientIndex = 0;
class ToneConnections {
constructor(tones, config) {
this.tones = tones;
this.combinationsOfTones = [...G.clone.combination(tones, 2)];
this.results = [];
this.mapToX = mapToXFor(tones, config);
this.colors = colorsFor(tones);
}
generate() {
this.combinationsOfTones.forEach(([tone1, tone2], index) => {
const point1 = { x: this.mapToX(tone1.x ?? tone1.frequency), y: mapToY(tone1.label) };
const point2 = { x: this.mapToX(tone2.x ?? tone2.frequency), y: mapToY(tone2.label) };
const curveHeight = Math.sqrt(5 * (point2.x - point1.x));
const controlPosition = (point2.x - point1.x) / 10;
const curve = {
start: point1,
control1: { x: point1.x + controlPosition, y: point1.y - curveHeight },
control2: { x: point2.x - controlPosition, y: point1.y - curveHeight },
end: point2
};
const xDist = curve.end.x - curve.start.x;
const yDist = curve.end.y - curve.start.y;
const ratioDiff = (tone2.ratio > tone1.ratio)
? tone2.ratio.div(tone1.ratio)
: tone1.ratio.div(tone2.ratio);
const text = {
x: curve.start.x + (xDist / 2) + 5,
y: curve.start.y + (yDist / 2) - 5,
angle: Math.atan(yDist / xDist) * (360 / (2 * Math.PI)),
content: ratioDiff.toFraction()
};
const gradient = this._findOrAddGradientFor(tone1, tone2);
this.results.push({ curve, gradient, text });
});
}
_findOrAddGradientFor(tone1, tone2) {
const color1 = this.colors[tone1.colorIndex];
const color2 = this.colors[tone2.colorIndex];
const key = [color1.toString(), color2.toString()].join("/");
if (key in gradients) {
return gradients[key];
} else {
lastGradientIndex++;
const gradient = {
from: color1,
to: color2,
id: `gradient-${lastGradientIndex}`
};
gradients[key] = gradient;
return gradient;
}
}
}
ToneConnections.generate = (tones, config) => {
const connections = new ToneConnections(tones, config);
connections.generate();
return connections.results;
}
return ToneConnections;
}
mapToXFor = (tones, options = {}) => {
const xAxis = options.xAxis || {};
const frequencies = tones.map(t => t.x ?? t.frequency);
const min = xAxis.min != null ? xAxis.min : d3.min(frequencies);
const max = xAxis.max != null ? xAxis.max : d3.max(frequencies);
let scale = d3.scaleLog()
.base(2)
.domain([min, max]);
if (xAxis.nice) {
scale = scale.nice();
}
scale = scale.range([lollimargin.left * 4, width - (lollimargin.right * 2)]);
return scale;
}
mapToYFor = (tones, options = {}) => {
const labels = tones.map(t => t.label);
const height = calculateGraphHeight(tones);
return d3.scalePoint()
.domain(labels)
.range([height - lollimargin.bottom, lollimargin.top])
.padding(0.5);
}
mapToY = index => {
return lollimargin.top + (rowHeight * index) + (rowHeight / 2);
}
colorsFor = tones => {
return lod.range(0, 360, 360 / numberOfColorsAmong(tones)).map(hue => d3.hcl(hue, 70, 80))
}
numberOfColorsAmong = (tones) => lod.uniqBy(tones, t => t.colorIndex).length
calculateGraphHeight = tones => {
const numberOfSeries = Object.keys(lod.countBy(tones, "seriesIndex")).length;
return lollimargin.top + (rowHeight * numberOfSeries) + lollimargin.bottom + 200;
}
lod = require("lodash@4.17.0")
G = require("generatorics@1.1.0")
Tone = require("tone@0.12.80")
Fraction = require("fraction.js@4.0.8")
colors = [...Array(10).keys()].map(x => x / 10).map(piecewiseColor).map(d3.color).map(x => x.hex())
hertz = origs.map(x => x / .864)
labels = [...Array(10).keys()].map(x => x + 40)
origs = labels.map(x => 14.1275 * 2 **(x / 10))html`
<style>
.colorAs {
background: hsl(${xetHue[0]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[0]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorAs3 {
background: hsl(${hues[0.40069]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.40069]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0158 {
background: hsl(${hues[0.0158]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.0158]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorC4 {
background: hsl(${hues[0.41302]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.41302]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorA4 {
background: hsl(${hues[0.49008]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.49008]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorB {
background: hsl(${xetHue[1]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[1]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorBc {
background: hsl(${bcHue} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${bcHue}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorC {
background: hsl(${xetHue[2]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[2]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorCs {
background: hsl(${xetHue[3]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[3]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD {
background: hsl(${xetHue[4]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[4]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDs {
background: hsl(${xetHue[5]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[5]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDds {
background: hsl(${ddsHue} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${ddsHue}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDsE {
background: hsl(${dseHue} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${dseHue}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorE {
background: hsl(${xetHue[6]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[6]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorF {
background: hsl(${xetHue[7]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[7]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorFs {
background: hsl(${xetHue[8]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[8]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorG {
background: hsl(${xetHue[9]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[9]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorGs {
background: hsl(${xetHue[10]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[10]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorA {
background: hsl(${xetHue[11]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${xetHue[11]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0 {
background: hsl(0 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(0, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color002 {
background: hsl(${hues[.002]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.002]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color004 {
background: hsl(${hues[.004]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.004]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color00390625 {
background: hsl(${hues[.00390625]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.00390625]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0058 {
background: hsl(${hues[.0058]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.0058]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0078125 {
background: hsl(${hues[.0078125]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.0078125]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color008 {
background: hsl(${hues[.008]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.008]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color014 {
background: hsl(${hues[.014]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[.014]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color015625 {
background: hsl(${hues[0.015625]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.015625]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color016 {
background: hsl(${hues[0.016]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.016]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color021 {
background: hsl(${hues[0.021]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.021]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color022 {
background: hsl(${hues[0.022]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.022]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color024 {
background: hsl(${hues[0.024]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.024]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color025 {
background: hsl(20 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(20, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color030 {
background: hsl(${hues[0.030]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.030]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color03125 {
background: hsl(${hues[0.03125]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.03125]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color039 {
background: hsl(${hues[0.039]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.039]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color040 {
background: hsl(${hues[0.040]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.040]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color050 {
background: hsl(24 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(24, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0625 {
background: hsl(${hues[0.0625]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.0625]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color065 {
background: hsl(${hues[0.065]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.065]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color067 {
background: hsl(${hues[0.067]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.067]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color26div300 {
background: hsl(${h26div300} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${h26div300}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color1 {
background: hsl(36 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(36, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color125 {
background: hsl(44 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(44, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color130 {
background: hsl(${hues[0.130]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.130]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color185 {
background: hsl(${hues[0.185]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.185]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color2 {
background: hsl(60 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(60, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color250 {
background: hsl(68 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(68, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color3 {
background: hsl(80 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(80, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorThird {
background: hsl(${hues[1/3]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[1/3]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color375 {
background: hsl(96 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(96, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color4 {
background: hsl(120 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(120, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color413 {
background: hsl(${hues[0.413]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.413]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color4132 {
background: hsl(${hues[0.4132]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.4132]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color420 {
background: hsl(${hues[0.420]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.420]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color450 {
background: hsl(${hues[0.450]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.450]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color460 {
background: hsl(${hues[0.460]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.460]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color480 {
background: hsl(${hues[0.480]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.480]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color490 {
background: hsl(${hues[0.490]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.490]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color5 {
background: hsl(180 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(180, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color599 {
background: hsl(${hues[0.599]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.599]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color6 {
background: hsl(208 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(208, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color625 {
background: hsl(216 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(216, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color7 {
background: hsl(240 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(240, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color704 {
background: hsl(${hues[0.704]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.704]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color750 {
background: hsl(264 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(264, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color754 {
background: hsl(${hues[0.754]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.754]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color788 {
background: hsl(${hues[0.788]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.788]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color8 {
background: hsl(276 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(276, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color815 {
background: hsl(${hues[0.815]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.815]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color864 {
background: hsl(${hues[0.864]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.864]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color875 {
background: hsl(292 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(292, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color9 {
background: hsl(300 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(300, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color935 {
background: hsl(${hues[0.935]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.935]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color950 {
background: hsl(328 ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(328, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color960 {
background: hsl(${hues[0.960]} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hues[0.960]}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD039 {
background: hsl(${hD039} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hD039}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD080 {
background: hsl(${hD080} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hD080}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorD285 {
background: hsl(${hD285} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hD285}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorff6300 {
background: #ff6300;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorffec00 {
background: #ffec00;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color99ff00 {
background: #99ff00;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color28ff00 {
background: #28ff00;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color00ffe8 {
background: #00ffe8;
color: black;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color007cff {
background: #007cff;
color: white;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color0800ff {
background: #0800ff;
color: white;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.color5e00d6 {
background: #5e00d6;
color: white;
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorIob {
background: hsl(${hIob} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hIob}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorMile {
background: hsl(${hMile} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hMile}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorPerS {
background: hsl(${hPerS} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hPerS}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorInch {
background: hsl(${hInch} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hInch}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorCuIn {
background: hsl(${hCuIn} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hCuIn}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorDrop {
background: hsl(${hDrop} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hDrop}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorMass {
background: hsl(${hMass} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hMass}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorCara {
background: hsl(${hCara} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hCara}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorGall {
background: hsl(${hGall} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hGall}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorBarr {
background: hsl(${hBarr} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hBarr}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorAvOz {
background: hsl(${hAvOz} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hAvOz}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorFlOz {
background: hsl(${hFlOz} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hFlOz}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorSqMi {
background: hsl(${hSqMi} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hSqMi}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
.colorSqIn {
background: hsl(${hSqIn} ${colorS / 10}% ${colorL / 10}%);
color: ${yiq(`hsl(${hSqIn}, ${colorS / 10}%, ${colorL / 10}%)`) > 0.51 ? "black" : "white"};
padding: 0px 5px;
border-radius: 4px;
font-weight: 400;
font-family: monospace;
}
</style>
`Glossary
- a: arcbeat, a hundred thousandth of a circle, 0.0036 degrees, 0.216 arcminutes, 12.96 arcseconds
-
b: beat, centimilliday, a hundred thousandth of an day, 864 milliseconds
- mb: millibeat, centimicroday, a thousandth of a beat, a hundred millionth of a day, 864 microseconds
- bpc: a musical or heart beat per centiday, a tenth of a beat per milliday, 0.0694 beats per minute, 100 beats per day
- bpm: a musical or heart beat per milliday, ten beats per centiday, 0.694 beats per minute, 1000 beats per day
- bmi: body mass index, kilograins of body mass divided by height in zem squared (kg/z²)
- c: taur, 𝜏r, 100000 kilozem, 40000 kilometers, nearly the circumference of the Earth, roughly the product of 𝜏 and the radius of the Earth, approximately the dividend of the surface area and the diameter of the Earth
-
d: day, a tenth of a decaday, a seventh of week, a fifth of a pentaday, 10 decidays, 24 hours, 100 centidays, 1000 millidays, 1440 minutes, 86400 seconds, 100000 beats, the inverse of a quotidie
- dek: decaday, a group of ten days, 2 pentadays
- pent: pentaday, a group of five days, half a decaday
- dod: day of decaday
- dop: day of pentaday
- dom: day of month
- dow: day of week
- doy: day of year, decaday * 10 + dod
- dd: deciday, a tenth of a day, 2.4 hours, 144 minutes
- cd: centiday, a hundredth of a day, 0.24 hours, 14.4 minutes
- md: milliday, a thousandth of a day, 1.44 minutes
- cmd: centimilliday, a hundred thousandth of a day, 1 beat, 864 milliseconds
- µd: microday, a millionth of a day, 86.4 milliseconds
- nd: nanoday, a billionth of a day, 86.4 microseconds
- °: degree, 1/360 turns, 180/𝜋 or 360/𝜏 radians
- e: egg, 1000 grains, 2 ounces, 64 grams
- ℓ: ell, cubit, 10/9 zem
- f: foot, 0.75 zem, 75 millimeter
- g: drop (gutta in Latin) or grain (granum in Latin), 64 microliters or 64 milligrams
- h: hand, 0.25 zem, 1 decimeter
- hex: hexadecimal, base 16
- hsl: hue saturation lightness
- hsv: hue saturation value
- i: inch, a sixteenth of a zem, 25 millimeter
- k: keg, cubic zem, 64 liters, 1000 wine glasses, a million drops, half a barrel
- L: liter, 15625 drops, a cubic decimeter
- m: meridian, a full circle around the Earth moving North or South; used in the abbreviations a.m. (antemeridian) and p.m. (postmeridian)
- m²: square meter, 6.25 square zem
- λ: parallel, a full circle around the Earth moving West or East; the English alphabet equivalent of λ is the letter “l”, which occurs three times in the word “parallel” and represents a line that crosses every possible longitude at the same latitude
- n: note, a specific frequency within an octave
-
o: octave, a two fold change in frequency
- do: decioctave, a tenth of a two fold change in frequency
-
p: perbeat, the inverse of a beat, 1/beat, once per beat, every beat, 100000 q; the letter “p” can be flipped vertically to produce the letter “b”
- Tp: teraperbeat, 1012 perbeat, the inverse of a picobeat, 1/picobeat, once per picobeat, every picobeat
- q: quotidie, the inverse of a day, a hundred thousandth of a perbeat; the letter “q” can be flipped vertically to produce the letter “d”
-
r: compass rose, a full circle along the horizon, 360 compass degress
- mr: compass millirose, a thousandth of a circle along the horizon, .36 compass degress
- rad: radian, \(1\over\tau\) turns, \(360\over\tau\) degrees, \(1\over 2\pi\) turns, \(180\over\pi\) degrees
- rgb: red green blue
- s: second, 1/90 millidays, 0.9 beats, 1 Dec second = 0.96 SI seconds
- SI: International System of Units
- sol: speed of light, 647.55170928 kiloomegars, 299792458 meters per second
- sos: speed of sound, 735.048 milliomegars, 340.3 meters per second
- 𝜏: 2𝜋 or approximately 6.2831853
- Tenet: ten equal temperament
- tod: time of day
- t: turn, 360 degrees, 𝜏 or 2𝜋 radians
- tzo: time zone offset
- u: ounce (uncia in Latin), 500 grains, 32 grams, 500 drops, 32 milliliters
- utc: Coordinated Universal Time
- US: United States
- v: omegar, ωr, 1041.6 miles per hour, 1.6 megameters per hour, 0.4629 kilometers per second, roughly 1.36 times the speed of sound
- w: wineglass, 64 milliters, 2 ounces, cubic decizem, 1000 drops
-
x: hexamilliare, square zem, z², 16 square decimeters, 1.7 square feet, 256 square inches
- Mx: megahexamilliare, a million square zem, square kilozem, kz², hexakilare, 16 hectares, 1600 ares, 40 acres, 0.16 square kilometers, 0.0625 square miles
- y: year
-
z: zem, zone equatorial meter, 4 decimeters, 16 inches
- kz²: square kilozem, a million square zem, megahexamilliare, Mx, hexakilare, 16 hectares, 1600 ares, 40 acres, 0.16 square kilometers, 0.0625 square miles
- kz: kilozem, 1000 zem, 400 meters, a quarter mile
- z²: square zem, hexamilliare, 16 square decimeters, 1.7 square feet, 256 square inches
- Dz²: square decazem, 1 hexadeciare, 16 square meters, 19.75 square yards, 100 square zem
- z³: cubic zem, 1 keg, 64 liters, 1000 wine glasses, a million drops, half a barrel
- dz³: cubic decizem, 1000 drops, 64 milliliters, 2 ounces, 1 wine glass
- cz³: cubic centizem, 1 drop, 64 microliters
- dz: decizem, a tenth of a zem, 4 centimeters
- cz: centizem, a hundredth of a zem, 4 millimeters
- mz: millizem, a thousandth of a zem, 0.4 millimeters
References
chrono-Compatible Low-Level Date Algorithms. https://howardhinnant.github.io/date_algorithms.
Footnotes
Newton, Issac. 1704. Opticks. https://doi.org/10.5479/sil.302475.39088000644674.↩︎
Clint Goss. 2022+098. Color of Sound. https://www.flutopedia.com/sound_color.htm.↩︎
Collignon, Claude Boniface. 1788. Découverte d’étalons justes, naturels, invariables et universels. https://archive.org/details/dcouvertedtalon00collgoog/page/n68/mode/2up.↩︎
Agnoli, Paolo & D’Agostini, Giulio. 2004+330. Why does the meter beat the second?. https://arxiv.org/abs/physics/0412078.↩︎
Hinnant, Howard. 2021+185.
chrono-Compatible Low-Level Date Algorithms. https://howardhinnant.github.io/date_algorithms.html.↩︎
