TimeClock Lab
TimeClock experiments: live recoloring, a liquid-glass theme, a stopwatch, and two-way pairings with DMC TimeInput, TimePicker, TimeGrid and DateTimePicker.
Overview
TimeClock experiments: live recoloring, a liquid-glass theme, a stopwatch, and two-way pairings with DMC TimeInput, TimePicker, TimeGrid and DateTimePicker.
Live examples
# File: docs/time_clock_lab/demo.py
"""TimeClock Lab demo — rendered on /time-clock-lab via `.. exec::`.
Ported verbatim from the pre-migration pages/time_clock_lab.py (same ids, same callbacks).
"""
import datetime as _dt
import dash_mantine_components as dmc
from dash import html, dcc, callback, Input, Output, State, ctx, no_update
from dash_iconify import DashIconify
from dash_mui_charts import TimeClock
# --------------------------------------------------------------------------- #
# Value helpers — normalise the many string formats to canonical "HH:MM:SS"
# --------------------------------------------------------------------------- #
def to_hms(value):
"""Any time-ish string (ISO, 'HH:mm', 'HH:mm:ss') -> 'HH:MM:SS' or None."""
if not value or not isinstance(value, str):
return None
part = value.split('T')[-1] if 'T' in value else value
bits = part.split(':')
if len(bits) < 2:
return None
try:
h, m = int(bits[0]), int(bits[1])
s = int(bits[2]) if len(bits) > 2 else 0
except ValueError:
return None
return f'{h:02d}:{m:02d}:{s:02d}'
def date_part(iso, fallback=None):
"""Extract the 'YYYY-MM-DD' date from an ISO datetime string."""
if iso and len(iso) >= 10 and iso[4] == '-':
return iso[:10]
return fallback or _dt.date.today().isoformat()
# --------------------------------------------------------------------------- #
# Layout helpers
# --------------------------------------------------------------------------- #
def clock_paper(component):
return dmc.Paper(component, withBorder=True, radius="md", p="xs",
style={"width": "fit-content"})
def lab_section(icon, title, description, *body):
return dmc.Card(
[
dmc.Group(
[
dmc.ThemeIcon(DashIconify(icon=icon, width=20),
variant="light", size="lg", radius="md"),
dmc.Title(title, order=3),
],
gap="sm",
),
dmc.Text(description, size="sm", c="dimmed", mt=4),
dmc.Divider(my="md"),
*body,
],
withBorder=True, radius="md", shadow="sm", p="lg", mb="xl",
)
SWATCHES = [
"#25262b", "#868e96", "#fa5252", "#e64980", "#be4bdb", "#7950f2",
"#4c6ef5", "#228be6", "#15aabf", "#12b886", "#40c057", "#82c91e",
"#fab005", "#fd7e14",
]
_TODAY = _dt.date.today().isoformat()
# Preset half-hour slots for the TimeGrid pairing (06:00 … 22:00)
TG_DATA = []
for _h in range(6, 23):
TG_DATA.append(f"{_h:02d}:00")
if _h < 22:
TG_DATA.append(f"{_h:02d}:30")
# --------------------------------------------------------------------------- #
# "View code" — professional, syntax-highlighted snippets via CodeHighlightTabs
# --------------------------------------------------------------------------- #
_PY_ICON = DashIconify(icon="vscode-icons:file-type-python", width=18)
_CSS_ICON = DashIconify(icon="vscode-icons:file-type-css", width=18)
def _code_blocks(tabs):
return dmc.CodeHighlightTabs(
code=[
{"fileName": name, "code": code, "language": lang,
"icon": _CSS_ICON if lang == "css" else _PY_ICON}
for name, code, lang in tabs
],
withExpandButton=True,
defaultExpanded=False,
maxCollapsedHeight=260,
)
def code_panel(tabs, **style):
"""Always-visible code panel (used inline, e.g. to the right of controls)."""
panel = _code_blocks(tabs)
panel.style = {"minWidth": 300, "flex": "1 1 360px",
"maxWidth": 600, "alignSelf": "stretch", **style}
return panel
def view_code(tabs):
"""Collapsed-by-default 'View code' disclosure with the example's source."""
return dmc.Accordion(
chevronPosition="left", variant="separated", radius="md", mt="md",
children=[
dmc.AccordionItem(
[
dmc.AccordionControl(
"View code",
icon=DashIconify(icon="mdi:code-tags", width=18),
),
dmc.AccordionPanel(_code_blocks(tabs)),
],
value="code",
),
],
)
# --- Faithful, condensed snippets for each example -------------------------- #
CODE = {}
CODE["colors_layout"] = '''from dash_mui_charts import TimeClock
import dash_mantine_components as dmc
# Face = ColorPicker with an alpha slider (rgba → opacity is adjustable);
# hand & numbers = compact ColorInputs.
dmc.ColorPicker(id="lab-color-face", format="rgba",
value="rgba(231, 245, 255, 0.9)",
swatches=SWATCHES, withPicker=True)
dmc.ColorInput(id="lab-color-hand", format="hex", value="#228be6")
dmc.ColorInput(id="lab-color-num", format="hex", value="",
placeholder="Theme default")
TimeClock(id="lab-color-clock", defaultValue="10:10:00",
views=["hours", "minutes", "seconds"], showViewSwitcher=True)
'''
CODE["colors_callback"] = '''from dash import callback, Input, Output, ctx, no_update
@callback(
Output("lab-color-clock", "sx"),
Output("lab-color-face", "value"),
Output("lab-color-hand", "value"),
Output("lab-color-num", "value"),
Input("lab-color-face", "value"), # rgba string → opacity supported
Input("lab-color-hand", "value"),
Input("lab-color-num", "value"),
Input("lab-color-reset", "n_clicks"),
)
def recolor_clock(face, hand, num, _reset):
is_reset = ctx.triggered_id == "lab-color-reset"
if is_reset:
face, hand, num = "rgba(231, 245, 255, 0.9)", "#228be6", ""
# Map each colour onto the MUI clock's internal classes via `sx`:
sx = {
"& .MuiClock-clock": {"backgroundColor": face},
"& .MuiClockPointer-root": {"backgroundColor": hand},
"& .MuiClockPointer-thumb": {"backgroundColor": hand,
"borderColor": hand},
"& .MuiClock-pin": {"backgroundColor": hand},
}
if num: # tint digits only when chosen
sx["& .MuiClockNumber-root"] = {"color": num}
echo = (face, hand, num) if is_reset else (no_update,) * 3
return (sx, *echo)
'''
CODE["glass_layout"] = '''# A normal TimeClock, themed entirely from CSS via a className.
html.Div(
TimeClock(id="lab-glass-clock", className="liquid-glass-clock",
value="10:09:00", ampm=False,
views=["hours", "minutes", "seconds"], showViewSwitcher=True),
className="liquid-glass-stage", # gradient backdrop for the frosted glass
)
'''
CODE["glass_css"] = '''/* assets/liquid_glass_clock.css (excerpt) */
/* Frosted-glass face + animated halo */
.liquid-glass-clock .MuiClock-clock {
background: linear-gradient(135deg,
rgba(255,255,255,.55), rgba(255,255,255,.20)) !important;
backdrop-filter: blur(14px) saturate(180%);
box-shadow: 0 10px 36px rgba(31,38,135,.18),
inset 0 1px 0 rgba(255,255,255,.9);
}
/* Magnifying-lens thumb — recentred on the digit (x:50%, y:-3px) */
.liquid-glass-clock .MuiClockPointer-thumb {
box-sizing: border-box !important;
width: 40px !important; height: 40px !important;
left: calc(50% - 20px) !important; top: -23px !important;
border: 1.5px solid rgba(255,255,255,.92) !important;
}
/* Bold + enlarge the digit the hand lands on */
.liquid-glass-clock .MuiClockNumber-selected {
font-size: 1.5rem !important; font-weight: 900 !important;
}
[data-mantine-color-scheme="dark"] .liquid-glass-clock .MuiClock-clock { /* … */ }
'''
CODE["stopwatch_layout"] = '''dcc.Interval(id="lab-sw-interval", interval=1000, disabled=True)
dcc.Store(id="lab-sw-store", data={"elapsed": 0, "running": False})
TimeClock(id="lab-sw-clock", value="00:00:00", view="seconds",
views=["hours", "minutes", "seconds"], readOnly=True)
dmc.Button("Start", id="lab-sw-start", color="green")
dmc.Button("Stop", id="lab-sw-stop", color="red")
dmc.Button("Restart", id="lab-sw-restart", color="gray")
'''
CODE["stopwatch_callback"] = '''@callback(
Output("lab-sw-store", "data"), Output("lab-sw-interval", "disabled"),
Output("lab-sw-clock", "value"), Output("lab-sw-clock", "view"),
Output("lab-sw-readout", "children"),
Input("lab-sw-interval", "n_intervals"),
Input("lab-sw-start", "n_clicks"), Input("lab-sw-stop", "n_clicks"),
Input("lab-sw-restart", "n_clicks"),
State("lab-sw-store", "data"), prevent_initial_call=True,
)
def run_stopwatch(_tick, _a, _b, _c, store):
elapsed, running = store["elapsed"], store["running"]
t = ctx.triggered_id
if t == "lab-sw-start": running = True
elif t == "lab-sw-stop": running = False
elif t == "lab-sw-restart": elapsed, running = 0, True
elif t == "lab-sw-interval" and running: elapsed += 1
h, m, s = elapsed // 3600, elapsed % 3600 // 60, elapsed % 60
# the view climbs with magnitude: seconds → minutes → hours
view = "seconds" if elapsed < 60 else "minutes" if elapsed < 3600 else "hours"
return ({"elapsed": elapsed, "running": running}, not running,
f"{h % 24:02d}:{m:02d}:{s:02d}", view, f"{h:02d}:{m:02d}:{s:02d}")
'''
CODE["sync_note"] = '''# Two-way sync pattern (used by every pairing below):
# • ONE @callback, with both components as Input AND Output.
# • Branch on ctx.triggered_id to find the source.
# • Return no_update for the source side → no feedback loop.
# • to_hms() normalises ISO / "HH:mm" / "HH:mm:ss" to "HH:MM:SS".
'''
CODE["timeinput_callback"] = '''dmc.TimeInput(id="lab-ti-input", withSeconds=True, value="08:30:00")
@callback(
Output("lab-ti-clock", "value"), Output("lab-ti-input", "value"),
Input("lab-ti-clock", "value"), Input("lab-ti-input", "value"),
Input("lab-ti-reset", "n_clicks"), Input("lab-ti-now", "n_clicks"),
Input("lab-ti-0900", "n_clicks"), Input("lab-ti-1730", "n_clicks"),
prevent_initial_call=True,
)
def sync_timeinput(clock_value, input_value, *_btns):
t = ctx.triggered_id
if t in PRESETS: # Reset / Now / 09:00 / 17:30
return PRESETS[t], PRESETS[t]
if t == "lab-ti-clock": # clock → input
return no_update, to_hms(clock_value)
return to_hms(input_value), no_update # input → clock
'''
CODE["timepicker_callback"] = '''dmc.TimePicker(id="lab-tp-input", withSeconds=True,
withDropdown=True, clearable=True, value="14:45:00")
@callback(
Output("lab-tp-clock", "value"), Output("lab-tp-input", "value"),
Input("lab-tp-clock", "value"), Input("lab-tp-input", "value"),
prevent_initial_call=True,
)
def sync_timepicker(clock_value, input_value):
if ctx.triggered_id == "lab-tp-clock":
return no_update, to_hms(clock_value) # clock → picker
return to_hms(input_value), no_update # picker → clock
'''
CODE["timegrid_callback"] = '''# Preset half-hour slots: ["06:00", "06:30", … "22:00"]
dmc.TimeGrid(id="lab-tg", data=TG_DATA, value="09:00", format="12h",
allowDeselect=True, w="100%",
simpleGridProps={"cols": {"base": 4, "sm": 6, "md": 8}})
@callback(
Output("lab-tg-clock", "value"), Output("lab-tg", "value"),
Input("lab-tg-clock", "value"), Input("lab-tg", "value"),
prevent_initial_call=True,
)
def sync_timegrid(clock_value, grid_value):
if ctx.triggered_id == "lab-tg": # slot → clock
return to_hms(grid_value), no_update
h, m, _ = (int(x) for x in to_hms(clock_value).split(":"))
slot = f"{h:02d}:{m:02d}" if m in (0, 30) and 6 <= h <= 22 else ""
return no_update, slot # clock → highlight slot (or clear)
'''
CODE["datetime_callback"] = '''dmc.DateTimePicker(id="lab-dtp", withSeconds=True,
value=f"{date.today()}T12:00:00")
@callback(
Output("lab-dtp-clock", "value"), Output("lab-dtp", "value"),
Input("lab-dtp-clock", "value"), Input("lab-dtp", "value"),
State("lab-dtp", "value"), prevent_initial_call=True,
)
def sync_datetime(clock_value, dtp_value, dtp_state):
if ctx.triggered_id == "lab-dtp-clock": # clock drives the TIME part
day = (dtp_state or "")[:10] or str(date.today())
return no_update, f"{day}T{to_hms(clock_value)}"
return to_hms(dtp_value), no_update # datetime → clock
'''
# --------------------------------------------------------------------------- #
# Layout
# --------------------------------------------------------------------------- #
component = dmc.Container(
[
dcc.Store(id="lab-sw-store", data={"elapsed": 0, "running": False}),
dcc.Interval(id="lab-sw-interval", interval=1000, disabled=True,
n_intervals=0),
# --- 1. Dynamic colours ---------------------------------------------
lab_section(
"mdi:palette-outline",
"1. Dynamic colours (dmc.ColorPicker + dmc.ColorInput)",
"Recolor the face, the hand/pointer, and the digit text independently "
"— fed straight into the TimeClock sx prop (.MuiClock-clock, "
".MuiClockPointer-root, .MuiClockNumber-root). The face uses a "
"ColorPicker with an alpha slider, so you can make it translucent "
"(rgba). Leave “Numbers” empty to keep the digits theme-coloured.",
dmc.Group(
[
clock_paper(
TimeClock(
id="lab-color-clock",
defaultValue="10:10:00",
views=["hours", "minutes", "seconds"],
showViewSwitcher=True,
)
),
dmc.Stack(
[
dmc.Stack(
[
dmc.Group(
[
DashIconify(icon="mdi:circle-opacity",
width=16),
dmc.Text("Clock face (with opacity)",
size="sm", fw=500),
],
gap=6,
),
dmc.ColorPicker(
id="lab-color-face",
value="rgba(231, 245, 255, 0.9)",
format="rgba",
swatches=SWATCHES,
swatchesPerRow=7,
withPicker=True,
w=240,
),
],
gap=4,
),
dmc.ColorInput(
id="lab-color-hand", label="Hand & pointer",
value="#228be6", format="hex", swatches=SWATCHES,
w=240,
leftSection=DashIconify(icon="mdi:clock-time-three-outline"),
),
dmc.ColorInput(
id="lab-color-num", label="Numbers / digits",
value="", format="hex", swatches=SWATCHES,
w=240,
placeholder="Theme default",
leftSection=DashIconify(icon="mdi:numeric"),
),
dmc.Button(
"Reset colours", id="lab-color-reset",
variant="subtle", size="xs", w=140,
leftSection=DashIconify(icon="mdi:restart"),
),
],
gap="md",
),
code_panel([
("layout.py", CODE["colors_layout"], "python"),
("callback.py", CODE["colors_callback"], "python"),
]),
],
align="flex-start", gap="xl",
),
),
# --- 2. Liquid glass clock ------------------------------------------
lab_section(
"mdi:blur",
"2. Liquid glass clock",
"An elevated, glassmorphic clock: a frosted liquid-glass face with an "
"animated halo, and a pointer thumb shaped like a magnifying lens that "
"enlarges the digit it lands on. Drag the hand and watch the lens "
"travel. Fully light/dark-mode aware — toggle the theme to see it shift.",
dmc.Group(
[
html.Div(
TimeClock(
id="lab-glass-clock",
className="liquid-glass-clock",
value="10:09:00",
views=["hours", "minutes", "seconds"],
ampm=False,
showViewSwitcher=True,
),
className="liquid-glass-stage",
style={"width": "fit-content"},
),
dmc.Stack(
[
dmc.Text("Selected time", size="sm", c="dimmed"),
dmc.Title(id="lab-glass-readout", children="10:09:00",
order=2, ff="monospace"),
dmc.Text(
"Face → frosted gradient + animated halo. "
"Thumb → magnifying lens (recentred & enlarged). "
"Selected digit → scaled up via font-size.",
size="xs", c="dimmed", maw=260,
),
],
gap="sm", justify="center",
),
],
align="center", gap="xl",
),
view_code([
("layout.py", CODE["glass_layout"], "python"),
("liquid_glass_clock.css", CODE["glass_css"], "css"),
]),
),
# --- 2. Stopwatch ---------------------------------------------------
lab_section(
"mdi:timer-outline",
"3. Stopwatch (dcc.Interval)",
"Start, stop, and restart a stopwatch. The clock view climbs with "
"magnitude — seconds until 0:60, then minutes until 60:00, then hours.",
dmc.Group(
[
clock_paper(
TimeClock(
id="lab-sw-clock",
value="00:00:00",
view="seconds",
views=["hours", "minutes", "seconds"],
readOnly=True,
)
),
dmc.Stack(
[
dmc.Group(
[
dmc.Text("Elapsed", size="sm", c="dimmed"),
dmc.Badge(id="lab-sw-unit", children="seconds",
variant="dot", color="teal"),
],
gap="xs",
),
# NOT a Title (sync 1.6.44 item 7's rider, which
# found this). `order=1` was being used for its
# SIZE, but it renders a real <h1> — so this page
# announced "00:00:00" as its top-level heading,
# below markdown.py's own order=2 page title, to
# every screen reader and every crawler reading
# the outline. A stopwatch readout is not a
# section of the document.
#
# Same pixels, no heading semantics, and
# aria-live so the value is actually announced as
# it changes — which an <h1> never was.
dmc.Text(
id="lab-sw-readout", children="00:00:00",
ff="monospace", fz="2.125rem", fw=700,
lh=1.3,
**{"aria-live": "polite",
"aria-label": "Elapsed time"},
),
dmc.Group(
[
dmc.Button(
"Start", id="lab-sw-start", color="green",
leftSection=DashIconify(icon="mdi:play"),
),
dmc.Button(
"Stop", id="lab-sw-stop", color="red",
variant="light",
leftSection=DashIconify(icon="mdi:pause"),
),
dmc.Button(
"Restart", id="lab-sw-restart", color="gray",
variant="outline",
leftSection=DashIconify(icon="mdi:restart"),
),
],
gap="xs",
),
],
gap="sm",
),
],
align="center", gap="xl",
),
view_code([
("layout.py", CODE["stopwatch_layout"], "python"),
("callback.py", CODE["stopwatch_callback"], "python"),
]),
),
# --- 3. TimeClock + TimeInput ---------------------------------------
lab_section(
"mdi:form-textbox",
"4. Paired with dmc.TimeInput + reset",
"Drag the clock or type in the native time input — they stay in sync. "
"The buttons reset or jump the selected time.",
dmc.Group(
[
clock_paper(
TimeClock(
id="lab-ti-clock", value="08:30:00",
views=["hours", "minutes", "seconds"],
showViewSwitcher=True,
)
),
dmc.Stack(
[
dmc.TimeInput(
id="lab-ti-input", label="TimeInput (HH:MM:SS)",
withSeconds=True, value="08:30:00", w=220,
leftSection=DashIconify(icon="mdi:clock-outline"),
),
dmc.Group(
[
dmc.Button("Reset", id="lab-ti-reset",
size="xs", variant="light",
color="red"),
dmc.Button("Now", id="lab-ti-now",
size="xs", variant="light"),
dmc.Button("09:00", id="lab-ti-0900",
size="xs", variant="default"),
dmc.Button("17:30", id="lab-ti-1730",
size="xs", variant="default"),
],
gap="xs",
),
dmc.Code(id="lab-ti-out", children="—"),
],
gap="sm",
),
],
align="flex-start", gap="xl",
),
view_code([
("pattern.py", CODE["sync_note"], "python"),
("timeinput.py", CODE["timeinput_callback"], "python"),
]),
),
# --- 4. TimeClock + TimePicker --------------------------------------
lab_section(
"mdi:clock-time-four-outline",
"5. Paired with dmc.TimePicker",
"A richer DMC time picker with a dropdown and seconds, kept in sync "
"with the clock both ways.",
dmc.Group(
[
clock_paper(
TimeClock(
id="lab-tp-clock", value="14:45:00",
views=["hours", "minutes", "seconds"],
showViewSwitcher=True,
)
),
dmc.Stack(
[
dmc.TimePicker(
id="lab-tp-input", label="TimePicker",
withSeconds=True, withDropdown=True,
clearable=True, value="14:45:00", w=220,
leftSection=DashIconify(icon="mdi:clock-edit-outline"),
),
dmc.Code(id="lab-tp-out", children="—"),
],
gap="sm",
),
],
align="flex-start", gap="xl",
),
view_code([
("timepicker.py", CODE["timepicker_callback"], "python"),
]),
),
# --- 5. TimeClock + TimeGrid ----------------------------------------
lab_section(
"mdi:view-grid-outline",
"6. Paired with dmc.TimeGrid",
"Pick a preset half-hour slot, or drag the clock — when it lands on a "
"slot the grid highlights it, otherwise the grid clears.",
dmc.Group(
[
clock_paper(
TimeClock(
id="lab-tg-clock", value="09:00:00",
views=["hours", "minutes"],
showViewSwitcher=True,
)
),
dmc.Code(id="lab-tg-out", children="—"),
],
align="center", gap="xl", mb="md",
),
dmc.TimeGrid(
id="lab-tg",
data=TG_DATA,
value="09:00",
withSeconds=False,
allowDeselect=True,
format="12h",
w="100%",
simpleGridProps={
"cols": {"base": 4, "xs": 5, "sm": 6, "md": 8},
"spacing": "xs",
"verticalSpacing": "xs",
},
),
view_code([
("timegrid.py", CODE["timegrid_callback"], "python"),
]),
),
# --- 6. TimeClock + DateTimePicker ----------------------------------
lab_section(
"mdi:calendar-clock",
"7. Paired with dmc.DateTimePicker",
"The clock controls the time portion of a full datetime — the date "
"you pick is preserved while the clock sets the hours/minutes/seconds.",
dmc.Group(
[
clock_paper(
TimeClock(
id="lab-dtp-clock", value="12:00:00",
views=["hours", "minutes", "seconds"],
showViewSwitcher=True,
)
),
dmc.Stack(
[
dmc.DateTimePicker(
id="lab-dtp", label="DateTimePicker",
withSeconds=True,
valueFormat="DD MMM YYYY HH:mm:ss",
value=f"{_TODAY}T12:00:00", w=260, clearable=True,
leftSection=DashIconify(icon="mdi:calendar-clock"),
),
dmc.Code(id="lab-dtp-out", children="—"),
],
gap="sm",
),
],
align="flex-start", gap="xl",
),
view_code([
("datetime.py", CODE["datetime_callback"], "python"),
]),
),
],
size="lg", px=0, py="md", className="dmc",
)
# --------------------------------------------------------------------------- #
# 1. Dynamic colours
# --------------------------------------------------------------------------- #
@callback(
Output("lab-color-clock", "sx"),
Output("lab-color-face", "value"),
Output("lab-color-hand", "value"),
Output("lab-color-num", "value"),
Input("lab-color-face", "value"),
Input("lab-color-hand", "value"),
Input("lab-color-num", "value"),
Input("lab-color-reset", "n_clicks"),
)
def recolor_clock(face, hand, num, _reset):
is_reset = ctx.triggered_id == "lab-color-reset"
if is_reset:
face, hand, num = "rgba(231, 245, 255, 0.9)", "#228be6", ""
sx = {
"& .MuiClock-clock": {"backgroundColor": face},
"& .MuiClockPointer-root": {"backgroundColor": hand},
"& .MuiClockPointer-thumb": {"backgroundColor": hand, "borderColor": hand},
"& .MuiClock-pin": {"backgroundColor": hand},
}
if num: # only tint the digits when a colour is chosen (else theme default)
sx["& .MuiClockNumber-root"] = {"color": num}
if is_reset:
return sx, face, hand, num
return sx, no_update, no_update, no_update
@callback(
Output("lab-glass-readout", "children"),
Input("lab-glass-clock", "value"),
prevent_initial_call=True,
)
def glass_readout(value):
return to_hms(value) or "—"
# --------------------------------------------------------------------------- #
# 2. Stopwatch
# --------------------------------------------------------------------------- #
@callback(
Output("lab-sw-store", "data"),
Output("lab-sw-interval", "disabled"),
Output("lab-sw-clock", "value"),
Output("lab-sw-clock", "view"),
Output("lab-sw-readout", "children"),
Output("lab-sw-unit", "children"),
Output("lab-sw-unit", "color"),
Input("lab-sw-interval", "n_intervals"),
Input("lab-sw-start", "n_clicks"),
Input("lab-sw-stop", "n_clicks"),
Input("lab-sw-restart", "n_clicks"),
State("lab-sw-store", "data"),
prevent_initial_call=True,
)
def run_stopwatch(_tick, _start, _stop, _restart, store):
store = store or {"elapsed": 0, "running": False}
elapsed = store.get("elapsed", 0)
running = store.get("running", False)
trigger = ctx.triggered_id
if trigger == "lab-sw-start":
running = True
elif trigger == "lab-sw-stop":
running = False
elif trigger == "lab-sw-restart":
elapsed, running = 0, True
elif trigger == "lab-sw-interval" and running:
elapsed += 1
h, m, s = elapsed // 3600, (elapsed % 3600) // 60, elapsed % 60
clock_value = f"{h % 24:02d}:{m:02d}:{s:02d}"
if elapsed < 60:
view, unit, color = "seconds", "seconds", "teal"
elif elapsed < 3600:
view, unit, color = "minutes", "minutes", "blue"
else:
view, unit, color = "hours", "hours", "grape"
readout = f"{h:02d}:{m:02d}:{s:02d}"
new_store = {"elapsed": elapsed, "running": running}
return new_store, (not running), clock_value, view, readout, unit, color
# --------------------------------------------------------------------------- #
# 3. TimeClock <-> TimeInput (+ reset / presets)
# --------------------------------------------------------------------------- #
@callback(
Output("lab-ti-clock", "value"),
Output("lab-ti-input", "value"),
Output("lab-ti-out", "children"),
Input("lab-ti-clock", "value"),
Input("lab-ti-input", "value"),
Input("lab-ti-reset", "n_clicks"),
Input("lab-ti-now", "n_clicks"),
Input("lab-ti-0900", "n_clicks"),
Input("lab-ti-1730", "n_clicks"),
prevent_initial_call=True,
)
def sync_timeinput(clock_value, input_value, _r, _n, _09, _17):
trigger = ctx.triggered_id
presets = {
"lab-ti-reset": "00:00:00",
"lab-ti-now": _dt.datetime.now().strftime("%H:%M:%S"),
"lab-ti-0900": "09:00:00",
"lab-ti-1730": "17:30:00",
}
if trigger in presets:
hms = presets[trigger]
return hms, hms, f"set → {hms}"
if trigger == "lab-ti-clock":
hms = to_hms(clock_value)
return no_update, hms, f"clock → {hms}"
if trigger == "lab-ti-input":
hms = to_hms(input_value)
if hms is None:
return no_update, no_update, "input cleared"
return hms, no_update, f"input → {hms}"
return no_update, no_update, no_update
# --------------------------------------------------------------------------- #
# 4. TimeClock <-> TimePicker
# --------------------------------------------------------------------------- #
@callback(
Output("lab-tp-clock", "value"),
Output("lab-tp-input", "value"),
Output("lab-tp-out", "children"),
Input("lab-tp-clock", "value"),
Input("lab-tp-input", "value"),
prevent_initial_call=True,
)
def sync_timepicker(clock_value, input_value):
trigger = ctx.triggered_id
if trigger == "lab-tp-clock":
hms = to_hms(clock_value)
return no_update, hms, f"clock → {hms}"
hms = to_hms(input_value)
if hms is None:
return no_update, no_update, "picker cleared"
return hms, no_update, f"picker → {hms}"
# --------------------------------------------------------------------------- #
# 5. TimeClock <-> TimeGrid (preset slots)
# --------------------------------------------------------------------------- #
@callback(
Output("lab-tg-clock", "value"),
Output("lab-tg", "value"),
Output("lab-tg-out", "children"),
Input("lab-tg-clock", "value"),
Input("lab-tg", "value"),
prevent_initial_call=True,
)
def sync_timegrid(clock_value, grid_value):
trigger = ctx.triggered_id
if trigger == "lab-tg":
hms = to_hms(grid_value)
if hms is None:
return no_update, no_update, "grid deselected"
return hms, no_update, f"grid → {hms}"
# clock changed: highlight a slot only when it lands on :00 / :30 in range
hms = to_hms(clock_value)
if hms is None:
return no_update, no_update, no_update
h, m, _s = (int(x) for x in hms.split(":"))
if m in (0, 30) and 6 <= h <= 22:
slot = f"{h:02d}:{m:02d}"
return no_update, slot, f"clock → slot {slot}"
return no_update, "", f"clock → {hms} (no slot)"
# --------------------------------------------------------------------------- #
# 6. TimeClock <-> DateTimePicker (clock drives the time portion)
# --------------------------------------------------------------------------- #
@callback(
Output("lab-dtp-clock", "value"),
Output("lab-dtp", "value"),
Output("lab-dtp-out", "children"),
Input("lab-dtp-clock", "value"),
Input("lab-dtp", "value"),
State("lab-dtp", "value"),
prevent_initial_call=True,
)
def sync_datetime(clock_value, dtp_value, dtp_state):
trigger = ctx.triggered_id
if trigger == "lab-dtp-clock":
hms = to_hms(clock_value)
if hms is None:
return no_update, no_update, no_update
day = date_part(dtp_state)
new_dt = f"{day}T{hms}"
return no_update, new_dt, f"clock → {new_dt}"
# DateTimePicker changed: pull its time onto the clock
hms = to_hms(dtp_value)
if hms is None:
return no_update, no_update, "datetime cleared"
return hms, no_update, f"datetime → {dtp_value}"
:defaultExpanded: false :withExpandedButton: true
Source: /time-clock-lab
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /time-clock-lab/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt