Public
Edited
Apr 23
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
coor = [[[-18, -90], [-18, 90], [18, 90], [18, -90], [-18, -90], ]]
Insert cell
viewof s = Inputs.range([0, 1000], {label: "satur", value: 1000, step: 1})
Insert cell
viewof l = Inputs.range([0, 1000], {label: "light", value: 800, step: 1})
Insert cell
viewof a = Inputs.range([0, 1000], {label: "alpha", value: 1000, step: 1})
Insert cell
hsla10 = [
`hsla(0, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // red
// `hsla(340, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // magentared
`hsla(300, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // magenta
`hsla(280, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // violet
`hsla(240, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // blue
`hsla(200, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // azure
`hsla(180, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // cyan
// `hsla(160, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // cyangreen
`hsla(120, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // green
`hsla(80, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // lime
`hsla(60, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // yellow
`hsla(40, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // orange
`hsla(0, ${s / 10}%, ${l / 10}%, ${a / 10}%)`, // red
]
Insert cell
deczones = [...Array(10).keys()].map(
i => ({
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [coor[0].map(t => [t[0]+36*i, t[1]])]},
"properties": []
})
)
Insert cell
// https://observablehq.com/@bayre/svg-swatches
function swatches({
colour,
swatchRadius = 7.5,
swatchPadding = swatchRadius * (2/3),
labelFont = "12px sans-serif",
labelFormat = x => x,
labelPadding = swatchRadius * 1.5,
marginLeft = 0
} = {}) {
const spacing = colour
.domain()
.map(d => labelFormat(d))
.map(d => getLabelLength(d, labelFont) + (swatchRadius * 2) + swatchPadding + labelPadding)
.map((_, i, g) => d3.cumsum(g)[i] + marginLeft)
const width = d3.max(spacing)
const height = swatchRadius * 2 + swatchPadding * 2
const svg = d3.create("svg")
.attr("id", "swatches")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("margin-top", "-12px")
.style("overflow", "visible")
.style("display", "block");
const g = svg
.append("g")
.attr("transform", `translate(0, ${height / 2})`)
.selectAll("g")
.data(colour.domain())
.join("g")
.attr("transform", (d, i) => `translate(${spacing[i - 1] || marginLeft}, 0)`);
g.append("circle")
.attr("fill", colour)
.attr("stroke", "black")
.attr("r", swatchRadius)
.attr("cx", swatchRadius)
.attr("cy", 0);
g.append("text")
.attr("x", swatchRadius * 2 + swatchPadding)
.attr("y", 0)
.attr("dominant-baseline", "central")
.style("font", labelFont)
.text(d => labelFormat(d));
return svg.node()
}
Insert cell
// adapted from https://observablehq.com/@mbostock/autosize-svg
getLabelLength = (label, labelFont = "12px sans-serif") => {
const id = DOM.uid("label").id;
const svg = html`<svg>
<style> .${id} { font: ${labelFont} } </style>
<g id=${id}>
<text class="${id}">${DOM.text(label)}</text>
</g>
</svg>`;

// Add the SVG element to the DOM so we can determine its size.
document.body.appendChild(svg);

// Compute the bounding box of the content.
const width = svg.getElementById(id).getBBox().width;

// Remove the SVG element from the DOM.
document.body.removeChild(svg);

return width;
}
Insert cell
timezones = FileAttachment("timezones.json").json()
Insert cell
zones = topojson.feature(timezones, timezones.objects.timezones).features
Insert cell
mesh = topojson.mesh(timezones, timezones.objects.timezones)
Insert cell
color = d3.scaleSequential(d3.interpolateRdBu).domain([14, -12])
Insert cell
Insert cell
// https://observablehq.com/@enjalot/draggable-world-map-coordinates-input
function worldMapCoordinates(config = {}, dimensions) {
const {
value = [], title, description, width = dimensions[0]
} = Array.isArray(config) ? {value: config} : config;
const height = dimensions[1];
let [lon, lat] = value;
lon = lon != null ? lon : null;
lat = lat != null ? lat : null;
const formEl = html`<form style="width: ${width}px;"></form>`;
const context = DOM.context2d(width, height);
const canvas = context.canvas;
canvas.style.margin = `-6px 0 ${width > 400 ? -12 : -18}px`;
const projection = config[2]
.precision(0.1)
.fitSize([width, height], { type: "Sphere" }).rotate([-153, 0]);
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();
}
// function formatLongitude(x) { return `${(x + 18) / 36}`; }
function draw() {
context.fillStyle = "#fff";
context.fillRect(0, 0, width, height);
if (!utctoggle) {
context.beginPath(); path({type: "Sphere"});
context.fillStyle = colors.ocean; context.fill();
if (dectoggle) {
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 = colors.land;
context.fill();
}
context.strokeStyle = `#888`;
context.stroke();
if (border) {
context.beginPath();
path(borders);
context.lineWidth = 1.25;
context.strokeStyle = `#888`;
context.stroke();
}
if (utctoggle) {
context.beginPath();
path(mesh);
context.lineWidth = 1.25;
context.strokeStyle = `#aaa`;
context.stroke();
}
if (dectoggle) {
context.beginPath();
path(graticule);
context.lineWidth = 1.25;
context.strokeStyle = `#000`;
context.stroke();
// context.font = "bold 24px serif";
// context.fillStyle = "#000";
// d3.range(-2, 342 + 1, 36).map((x, i) => context.fillText(i, projection([x, 90])[0] - (width < 400) * 4, 40 - (width < 400) * 20));
// context.fillStyle = "#000";
// d3.range(-2, 342 + 1, 36).map((x, i) => context.fillText(i, projection([x, 90])[0] - (width < 400) * 4, 578 - (width < 400) * 281 - (width < 800) * 120));
// 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.4)";
context.fill();
context.beginPath();
path.pointRadius(28);
path({type: "Point", coordinates: sun});
context.strokeStyle = "#0009";
context.fillStyle = "#ff0b";
context.lineWidth = 1;
context.stroke();
context.fill();
}
}
draw();
return formEl;
}
Insert cell
projections = [
{name: "CylindricalEqualArea", value: d3.geoCylindricalEqualArea},
{name: "CylindricalStereographic", value: d3.geoCylindricalStereographic},
{name: "Eckert1", value: d3.geoEckert1},
{name: "Eckert2", value: d3.geoEckert2},
{name: "Eckert3", value: d3.geoEckert3},
{name: "Eckert4", value: d3.geoEckert4},
{name: "Eckert5", value: d3.geoEckert5},
{name: "Eckert6", value: d3.geoEckert6},
{name: "Equirectangular", value: d3.geoEquirectangular},
{name: "Hufnagel", value: d3.geoHufnagel},
{name: "Kavrayskiy7", value: d3.geoKavrayskiy7},
{name: "Miller", value: d3.geoMiller},
{name: "MtFlatPolarParabolic", value: d3.geoMtFlatPolarParabolic},
{name: "MtFlatPolarQuartic", value: d3.geoMtFlatPolarQuartic},
{name: "MtFlatPolarSinusodial", value: d3.geoMtFlatPolarSinusoidal},
{name: "NaturalEarth1", value: d3.geoNaturalEarth1},
{name: "NaturalEarth2", value: d3.geoNaturalEarth2},
{name: "NellHammer", value: d3.geoNellHammer},
{name: "Patterson", value: d3.geoPatterson},
{name: "Robinson", value: d3.geoRobinson},
{name: "Times", value: d3.geoTimes},
{name: "Wagner4", value: d3.geoWagner4},
{name: "Wagner6", value: d3.geoWagner6},
{name: "Winkel3", value: d3.geoWinkel3},
]
Insert cell
Insert cell
colors = ({
night: "#719fb6",
day: "#ffe438",
grid: "#4b6a79",
ocean: "#adeeff",
land: "#f5f1dc",
sun: "#ffb438"
})
Insert cell
Insert cell
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;
}
Insert cell
function long2zone(degrees = -180) {
return Math.floor(long2turn(degrees, 1));
}
Insert cell
projection = select.value().rotate([-148, 0])
Insert cell
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)];
}
Insert cell
night = d3.geoCircle()
.radius(90)
.center(antipode(sun))
()
Insert cell
antipode = ([longitude, latitude]) => [longitude + 180, -latitude]
Insert cell
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;
}
Insert cell
d3 = require("d3@6", "d3-array@3", "d3-geo@3", "d3-geo-projection@4", "d3-geo-polygon@1.8")
Insert cell
sphere = ({type: "Sphere"})
Insert cell
graticule = d3.geoGraticule().stepMinor([36,0]).stepMajor([36,0])()
Insert cell
graticule.coordinates = graticule.coordinates.map(
i => i.map(j => j.map((k, index, arr) => i.length === 3 && index === 0 ? k - 18 : k))
)
Insert cell
land = topojson.feature(world, world.objects.land)
Insert cell
world = fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/land-50m.json").then(response => response.json())
Insert cell
topojson = require("topojson-client@3")
Insert cell
solar = require("solar-calculator@0.3/dist/solar-calculator.min.js")
Insert cell
borders = topojson.mesh(countries, countries.objects.countries, (a, b) => a !== b)
Insert cell
countries = fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json").then(response => response.json())
Insert cell
<style>
div:has(> svg#swatches) {
display: flex;
justify-content: center;
}
</style>
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more