Records of my newborn naps - split at midnight

Good morning,

I have been trying for several days to correctly handle the naps of my newborn to create a dashboard that shows how long he sleeps per day.

I am recording every nap in influxdb with 3 fields:

  • start
  • stop
  • duration (the seconds passed between start and stop)

I am able to correctly group the naps per day by using the “start” field. However, everyday it happens that a sleep cycle crosses the midnight. This sleeping cycle is not correctly handled because it is recorded in the previous day instead of being split across the two days.

Here below is the script I have been using for the dashboard that does NOT handle the midnight. Would you have any suggestion about how to modify it to split the naps across midnight correctly?

Many thanks in advance!

import "math"
import "date"  // Required for timezone-aware date functions
import "timezone"

option location = timezone.location(name: "Europe/Madrid")

raw = from(bucket: "baby-care")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => 
    r._measurement == "baby_events" and 
    r.event_type == "Nap" and 
    (r._field == "start" or r._field == "duration")
  )

pivoted = raw
  |> pivot(
    rowKey: ["_time", "parent", "user_id", "event_type"],
    columnKey: ["_field"],
    valueColumn: "_value"
  )
  |> filter(fn: (r) => exists r.start and exists r.duration and r.start != "")

clean = pivoted
  |> map(fn: (r) => ({
      _time: time(v: r.start),        // Must be valid ISO string
      _value: float(v: r.duration),   // Ensure numeric
      _field: "duration",
      _measurement: "baby_events"
  }))
  |> drop(columns: ["_start", "_stop"])  

clean
  |> map(fn: (r) => ({ 
      r with 
      _time: date.truncate(t: r._time, unit: 1d)
  }))
  |> truncateTimeColumn(unit: 1d)
  |> group(columns: ["_value"], mode: "except")
  |> sum()
  |> group(columns: ["_value", "_time"], mode: "except")
  |> map(fn: (r) => ({
      r with
      _value: math.round(x: r._value / 3600.0 * 100.0) / 100.0  // seconds → hours
  }))
  |> rename(columns: { _time: "Day", _value: "Total Nap Duration (hours)" })

@borelg I understand the problem you’re trying to solve, but would you be willing to share a sample of your raw dataset? I think it’d help me to better understand how to help.

Hi Scott, thanks!

Sure, unfortunately as a new user I cannot upload attachment. Please find below an extraction of the past 24h. You might visualize them better with Excel.

table_result	_measurementgroupstring	_fieldgroupstring	_valueno grouplong	_startgroupdateTime:RFC3339	_stopgroupdateTime:RFC3339	_timeno groupdateTime:RFC3339	event_typegroupstring	parentgroupstring	user_idgroupstring
0	baby_events	duration	2969	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T13:20:00.000Z	Nap	Chiara	10000000
0	baby_events	duration	11183	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T23:11:23.716Z	Nap	Chiara	10000000
									
table_result	_measurementgroupstring	_fieldgroupstring	_valueno groupstring	_startgroupdateTime:RFC3339	_stopgroupdateTime:RFC3339	_timeno groupdateTime:RFC3339	event_typegroupstring	parentgroupstring	user_idgroupstring
1	baby_events	start	2025-08-25T14:30:30+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T13:20:00.000Z	Nap	Chiara	10000000
1	baby_events	start	2025-08-25T22:05:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T23:11:23.716Z	Nap	Chiara	10000000
2	baby_events	stop	2025-08-25T15:20:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T13:20:00.000Z	Nap	Chiara	10000000
2	baby_events	stop	2025-08-26T01:11:23+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T23:11:23.716Z	Nap	Chiara	10000000
									
table_result	_measurementgroupstring	_fieldgroupstring	_valueno grouplong	_startgroupdateTime:RFC3339	_stopgroupdateTime:RFC3339	_timeno groupdateTime:RFC3339	event_typegroupstring	parentgroupstring	user_idgroupstring
3	baby_events	duration	7954	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T10:42:34.788Z	Nap	Gabriele	20000000
3	baby_events	duration	2407	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T15:20:00.000Z	Nap	Gabriele	20000000
3	baby_events	duration	10800	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T19:40:00.000Z	Nap	Gabriele	20000000
3	baby_events	duration	7462	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-26T02:09:22.281Z	Nap	Gabriele	20000000
3	baby_events	duration	7980	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-26T05:28:00.662Z	Nap	Gabriele	20000000
									
table_result	_measurementgroupstring	_fieldgroupstring	_valueno groupstring	_startgroupdateTime:RFC3339	_stopgroupdateTime:RFC3339	_timeno groupdateTime:RFC3339	event_typegroupstring	parentgroupstring	user_idgroupstring
4	baby_events	start	2025-08-25T10:30:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T10:42:34.788Z	Nap	Gabriele	20000000
4	baby_events	start	2025-08-25T16:39:52+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T15:20:00.000Z	Nap	Gabriele	20000000
4	baby_events	start	2025-08-25T18:40:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T19:40:00.000Z	Nap	Gabriele	20000000
4	baby_events	start	2025-08-26T02:05:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-26T02:09:22.281Z	Nap	Gabriele	20000000
4	baby_events	start	2025-08-26T05:15:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-26T05:28:00.662Z	Nap	Gabriele	20000000
									
table_result	_measurementgroupstring	_fieldgroupstring	_valueno groupstring	_startgroupdateTime:RFC3339	_stopgroupdateTime:RFC3339	_timeno groupdateTime:RFC3339	event_typegroupstring	parentgroupstring	user_idgroupstring
5	baby_events	stop	2025-08-25T12:42:34+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T10:42:34.788Z	Nap	Gabriele	20000000
5	baby_events	stop	2025-08-25T17:20:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T15:20:00.000Z	Nap	Gabriele	20000000
5	baby_events	stop	2025-08-25T21:40:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-25T19:40:00.000Z	Nap	Gabriele	20000000
5	baby_events	stop	2025-08-26T04:09:22+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-26T02:09:22.281Z	Nap	Gabriele	20000000
5	baby_events	stop	2025-08-26T07:28:00+02:00	2025-08-25T08:13:56.492Z	2025-08-26T08:13:56.492Z	2025-08-26T05:28:00.662Z	Nap	Gabriele	20000000
									

This is how it looks like in InfluxDB:

For the sake of clarity here below you find the snippet of python I use to write data to InfluxDB if it may clarify the structure of the data:

if write_api and influx_client:
        try:
            point = (
                Point("baby_events")
                .tag("event_type", "Nap")
                .tag("parent", who)
                .tag("user_id", str(user_id) if user_id else "unknown")
                .field("start", start.astimezone(TZ).isoformat(timespec="seconds"))
                .field("stop", italian_stop.isoformat(timespec="seconds"))
                .field("duration", duration_sec)  # seconds (numeric)
                .time(stop)  # point timestamp = end time
            )
            write_api.write(bucket=INFLUXDB_BUCKET, org=INFLUXDB_ORG, record=point)
            print(f"✅ Saved Nap to InfluxDB: {who}, duration {duration_sec}s")
        except Exception as e:
            print(f"❌ InfluxDB write failed (Nap): {e}")

Thanks again!

Ok, this is a little hacky, but it works. It essentially transforms your nap start/stop data into state-based data (per minute). So it structures the data where each row represents a minute and the nap state during that minute. It then calculates the duration of each state each day. Note: I included the 2h offset in the last aggregateWindow() call to account for your timezone offset.

raw
  |> group(columns: ["_time","_value"], mode: "except")
  |> map(fn: (r) => ({
    _time: time(v: r._value),
    _field: "sleep_state",
    sleep_state: if r._field == "start" then "asleep" else "awake",
    _value: 0
  }))
  |> group(columns: ["_field"])
  |> sort(columns: ["_time"])
  |> aggregateWindow(every: 1m, fn: last, createEmpty: true)
  |> fill(column: "sleep_state", usePrevious: true)
  |> filter(fn: (r) => exists r.sleep_state)
  |> events.duration(unit: 1m, columnName: "duration")
  |> group(columns: ["sleep_state"])
  |> map(fn: (r) => ({r with _value: float(v: r.duration) / 60.0}))
  |> aggregateWindow(every: 1d, fn: sum, offset: 2h)
  |> pivot(rowKey:["_time"], columnKey: ["sleep_state"], valueColumn: "_value")

Thanks Scott!

It worked very well!

Much appreciated :slight_smile: