Public
Edited
Feb 28
Insert cell
Insert cell
// This is what will be displayed in the Observable notebook
html`${styles}${wordClock}`
Insert cell
Insert cell
// Define the word grid as a 2D array with semantic meaning
wordGrid = [
[["it", "word"], ["is", "word"], ["half", "half"], ["ten", "minute_word"]], // First row - "ten" for minutes
[["quarter", "minute_word"], ["twenty", "minute_word"]],
[["five", "minute_word"], ["minutes", "word"], ["to", "word"]],
[["past", "word"], ["one", "hour_word"], ["three", "hour_word"]],
[["two", "hour_word"], ["four", "hour_word"], ["five", "hour_word"]], // "five" as hour
[["six", "hour_word"], ["seven", "hour_word"], ["eight", "hour_word"]],
[["nine", "hour_word"], ["ten", "hour_word"], ["eleven", "hour_word"]], // "ten" as hour
[["twelve", "hour_word"], ["o'clock", "word"]]
]
Insert cell
Insert cell
// Map of simple word positions for highlighting
wordPositions = Object.fromEntries([
["it", [0, 0]],
["is", [0, 1]],
["half", [0, 2]],
["quarter", [1, 0]],
["twenty", [1, 1]],
["five_min", [2, 0]], // Semantic identifier for "five" as minute
["minutes", [2, 1]],
["to", [2, 2]],
["past", [3, 0]],
["one", [3, 1]],
["three", [3, 2]],
["two", [4, 0]],
["four", [4, 1]],
["five_hour", [4, 2]], // Semantic identifier for "five" as hour
["six", [5, 0]],
["seven", [5, 1]],
["eight", [5, 2]],
["nine", [6, 0]],
["ten_hour", [6, 1]], // Semantic identifier for "ten" as hour
["ten_min", [0, 3]], // Semantic identifier for "ten" as minute
["eleven", [6, 2]],
["twelve", [7, 0]],
["oclock", [7, 1]]
])
Insert cell
Insert cell
// Map numbers to their word positions for hours
hourToPosition = new Map([
[1, "one"],
[2, "two"],
[3, "three"],
[4, "four"],
[5, "five_hour"], // Use semantic identifier
[6, "six"],
[7, "seven"],
[8, "eight"],
[9, "nine"],
[10, "ten_hour"], // Use semantic identifier
[11, "eleven"],
[12, "twelve"]
])
Insert cell
Insert cell
// Define a function to format hours for 12-hour clock
getHour = hour => {
if (hour === 0 || hour === 12) return 12;
return hour % 12;
}
Insert cell
// Get next hour for "to" times
getNextHour = hour => {
const nextHour = (hour + 1) % 24;
return getHour(nextHour);
}
Insert cell
Insert cell
// Determine which words should be highlighted based on the current time
getHighlightedWords = (date) => {
const hour = date.getHours();
const minute = date.getMinutes();
// Store words to highlight (using the semantic identifiers)
const words = new Set();
// Always highlight "it is"
words.add("it");
words.add("is");
// Get hour words
const hourWord = hourToPosition.get(getHour(hour));
const nextHourWord = hourToPosition.get(getNextHour(hour));
// Handle minutes
if (minute >= 0 && minute < 5) {
words.add("oclock");
words.add(hourWord);
} else if (minute >= 5 && minute < 10) {
words.add("five_min"); // Use semantic identifier for "five" as minute
words.add("minutes");
words.add("past");
words.add(hourWord);
} else if (minute >= 10 && minute < 15) {
words.add("ten_min"); // Use semantic identifier for "ten" as minute
words.add("minutes");
words.add("past");
words.add(hourWord);
} else if (minute >= 15 && minute < 20) {
words.add("quarter");
words.add("past");
words.add(hourWord);
} else if (minute >= 20 && minute < 25) {
words.add("twenty");
words.add("minutes");
words.add("past");
words.add(hourWord);
} else if (minute >= 25 && minute < 30) {
words.add("twenty");
words.add("five_min"); // Use semantic identifier for "five" as minute
words.add("minutes");
words.add("past");
words.add(hourWord);
} else if (minute >= 30 && minute < 35) {
words.add("half");
words.add("past");
words.add(hourWord);
} else if (minute >= 35 && minute < 40) {
words.add("twenty");
words.add("five_min"); // Use semantic identifier for "five" as minute
words.add("minutes");
words.add("to");
words.add(nextHourWord);
} else if (minute >= 40 && minute < 45) {
words.add("twenty");
words.add("minutes");
words.add("to");
words.add(nextHourWord);
} else if (minute >= 45 && minute < 50) {
words.add("quarter");
words.add("to");
words.add(nextHourWord);
} else if (minute >= 50 && minute < 55) {
words.add("ten_min"); // Use semantic identifier for "ten" as minute
words.add("minutes");
words.add("to");
words.add(nextHourWord);
} else {
words.add("five_min"); // Use semantic identifier for "five" as minute
words.add("minutes");
words.add("to");
words.add(nextHourWord);
}
// Convert words to coordinates
const coordinates = [];
for (const word of words) {
if (wordPositions[word]) {
coordinates.push(wordPositions[word]);
}
}
return coordinates;
}
Insert cell
Insert cell
currentTime = {
const getCurrentTime = () => {
// Calculate timestamp aligned to the current minute
const now = Date.now();
const alignedTimestamp = Math.floor(now / 60000) * 60000;
return new Date(alignedTimestamp);
}
return getCurrentTime();
}
Insert cell
Insert cell
// Define CSS styles for the word clock
styles = html`<style>
.word-clock {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: #111;
border-radius: 8px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
padding: 25px;
width: 300px;
margin: 0;
}
.word-row {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: 10px 0;
}
.word {
margin: 0 8px;
padding: 4px;
color: rgba(255, 255, 255, 0.5); /* Dimmed white for non-highlighted words */
text-transform: uppercase; /* Make all text capital letters */
font-weight: 700; /* Make font bold - 700 is bold weight */
letter-spacing: 1px; /* Add some spacing between letters */
transition: all 0.3s ease;
}
.highlighted {
font-weight: 900; /* Extra bold for highlighted words */
color: #FF7E00; /* Bright orange for highlighted words */
text-shadow: 0 0 3px rgba(255, 126, 0, 0.3); /* Subtle glow effect */
}
.time-display {
text-align: center;
font-size: 14px;
margin-bottom: 20px;
color: rgba(255, 255, 255, 0.7); /* Slightly brighter white for time display */
text-transform: uppercase; /* Make time display capital letters too */
font-weight: bold;
letter-spacing: 1px; /* Add some spacing between letters */
}
</style>`
Insert cell
Insert cell
// Create the word clock display with direct updating
wordClock = {
// Create initial display with current time
const container = document.createElement('div');
container.className = 'word-clock';
container.id = 'word-clock-container';
// Function to update the clock
const updateClock = () => {
const now = new Date();
const highlightedCoordinates = getHighlightedWords(now);
// Clear existing content
container.innerHTML = '';
// Add current time display
const timeDisplay = document.createElement('div');
timeDisplay.className = 'time-display';
timeDisplay.textContent = `Current time: ${now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
container.appendChild(timeDisplay);
// Add each row of words
wordGrid.forEach((row, rowIndex) => {
const rowEl = document.createElement('div');
rowEl.className = 'word-row';
row.forEach((item, colIndex) => {
const word = typeof item === 'string' ? item : item[0]; // Get the word from the grid
const wordEl = document.createElement('span');
wordEl.className = 'word';
wordEl.textContent = word;
// Check if this word's coordinates are in the highlighted list
const isHighlighted = highlightedCoordinates.some(
coord => coord && coord[0] === rowIndex && coord[1] === colIndex
);
if (isHighlighted) {
wordEl.classList.add('highlighted');
}
rowEl.appendChild(wordEl);
});
container.appendChild(rowEl);
});
};
// Initial update
updateClock();
// Set interval to update every minute
const interval = setInterval(updateClock, 60000);
// Clean up on invalidation
invalidation.then(() => clearInterval(interval));
return container;
}
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