Best use case

sleep-charts is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Teams using sleep-charts should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/sleep-charts/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/healthcare-wellness/sleep-tracker/skills/sleep-charts/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/sleep-charts/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How sleep-charts Compares

Feature / Agentsleep-chartsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

This skill provides specific capabilities for your AI agent. See the About section for full details.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# sleep-charts Skill

Chart.js 4 patterns for sleep data visualization: duration trend lines, stage doughnut charts, monthly calendar heatmap, and sleep debt bar charts.

## When to use

Use this skill when building:
- Duration or quality trend line charts with goal reference lines
- Sleep stage doughnut (StagePie) component
- Monthly calendar heatmap
- Sleep debt daily bar chart
- SleepBar timeline component

---

## Global Chart.js defaults

```typescript
// src/client/lib/chartConfig.ts
import {
  Chart,
  LineElement, PointElement, LineController,
  BarElement, BarController,
  DoughnutController, ArcElement,
  CategoryScale, LinearScale,
  Tooltip, Legend, Filler,
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';

Chart.register(
  LineElement, PointElement, LineController,
  BarElement, BarController,
  DoughnutController, ArcElement,
  CategoryScale, LinearScale,
  Tooltip, Legend, Filler,
  annotationPlugin,
);

Chart.defaults.font.family = "'Instrument Sans', system-ui, sans-serif";
Chart.defaults.color = '#78716c';
Chart.defaults.borderColor = '#e7e5e4';
```

---

## Duration trend line chart

```typescript
import type { ChartConfiguration } from 'chart.js';
import type { AnnotationOptions } from 'chartjs-plugin-annotation';

interface WeeklyPoint {
  date: string;
  duration_min: number;
  quality_score: number;
}

export function buildDurationTrendConfig(
  data: WeeklyPoint[],
  goalMin: number,
): ChartConfiguration<'line'> {
  const labels = data.map(d => d.date);
  const values = data.map(d => d.duration_min / 60); // convert to hours
  const goalHours = goalMin / 60;

  const goalAnnotation: AnnotationOptions = {
    type: 'line',
    yMin: goalHours,
    yMax: goalHours,
    borderColor: '#d97706',
    borderWidth: 1.5,
    borderDash: [4, 3],
    label: {
      content: `Goal ${goalHours}h`,
      display: true,
      position: 'start',
      color: '#d97706',
      font: { size: 10 },
    },
  };

  return {
    type: 'line',
    data: {
      labels,
      datasets: [{
        label: 'Duration',
        data: values,
        borderColor: '#0891b2',
        backgroundColor: 'rgba(8,145,178,0.08)',
        fill: true,
        tension: 0.3,
        pointRadius: 3,
        pointHoverRadius: 5,
        pointBackgroundColor: '#0891b2',
      }],
    },
    options: {
      responsive: true,
      plugins: {
        legend: { display: false },
        annotation: { annotations: { goalLine: goalAnnotation } },
        tooltip: {
          callbacks: {
            label(ctx) {
              const h = Math.floor(ctx.parsed.y);
              const m = Math.round((ctx.parsed.y - h) * 60);
              return `${h}h ${m}m`;
            },
          },
        },
      },
      scales: {
        y: {
          title: { display: true, text: 'Hours' },
          min: 0,
          max: 12,
        },
      },
    },
  };
}
```

---

## Quality trend line chart

```typescript
export function buildQualityTrendConfig(
  data: WeeklyPoint[],
  goalScore: number,
): ChartConfiguration<'line'> {
  const goalAnnotation: AnnotationOptions = {
    type: 'line',
    yMin: goalScore,
    yMax: goalScore,
    borderColor: '#d97706',
    borderWidth: 1.5,
    borderDash: [4, 3],
  };

  return {
    type: 'line',
    data: {
      labels: data.map(d => d.date),
      datasets: [{
        label: 'Quality',
        data: data.map(d => d.quality_score),
        borderColor: '#16a34a',
        backgroundColor: 'rgba(22,163,74,0.08)',
        fill: true,
        tension: 0.3,
        pointRadius: 3,
        pointHoverRadius: 5,
      }],
    },
    options: {
      responsive: true,
      plugins: {
        legend: { display: false },
        annotation: { annotations: { goalLine: goalAnnotation } },
      },
      scales: {
        y: {
          min: 0,
          max: 10,
          title: { display: true, text: 'Score' },
        },
      },
    },
  };
}
```

---

## StagePie component (Chart.js doughnut)

```typescript
// src/client/components/StagePie.tsx
import { useEffect, useRef } from 'react';
import Chart from 'chart.js/auto';

interface StagePieProps {
  deep_pct: number;
  light_pct: number;
  rem_pct: number;
  awake_pct: number;
  durationMin: number;
}

export function StagePie({ deep_pct, light_pct, rem_pct, awake_pct, durationMin }: StagePieProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const chartRef = useRef<Chart | null>(null);
  const h = Math.floor(durationMin / 60);
  const m = durationMin % 60;

  useEffect(() => {
    if (!canvasRef.current) return;
    chartRef.current?.destroy();

    chartRef.current = new Chart(canvasRef.current, {
      type: 'doughnut',
      data: {
        labels: ['Deep', 'Light', 'REM', 'Awake'],
        datasets: [{
          data: [deep_pct, light_pct, rem_pct, awake_pct],
          backgroundColor: ['#1e40af', '#7dd3fc', '#7c3aed', '#d1d5db'],
          borderWidth: 0,
          hoverOffset: 4,
        }],
      },
      options: {
        cutout: '62%',
        plugins: {
          legend: { display: false },
          tooltip: {
            callbacks: {
              label(ctx) { return `${ctx.label}: ${ctx.parsed}%`; },
            },
          },
        },
      },
    });

    return () => { chartRef.current?.destroy(); };
  }, [deep_pct, light_pct, rem_pct, awake_pct]);

  return (
    <figure
      aria-label={`Sleep stages: Deep ${deep_pct}%, Light ${light_pct}%, REM ${rem_pct}%, Awake ${awake_pct}%`}
      style={{ position: 'relative', width: 140, height: 140, margin: '0 auto' }}
    >
      <canvas ref={canvasRef} width={140} height={140} />
      <div style={{
        position: 'absolute', inset: 0,
        display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'center',
        pointerEvents: 'none',
      }}>
        <span style={{ fontFamily: 'var(--font-mono)', fontSize: 14, fontWeight: 700 }}>
          {h}h {m}m
        </span>
      </div>
    </figure>
  );
}
```

---

## SleepBar component

```typescript
// src/client/components/SleepBar.tsx
// Timeline from 20:00 to 12:00 next day = 16 hours = 960 minutes

const TIMELINE_START_HOUR = 20; // 20:00
const TIMELINE_DURATION_MIN = 960; // 16 hours

function toMinutesSince20(isoDatetime: string): number {
  const d = new Date(isoDatetime);
  const h = d.getHours();
  const m = d.getMinutes();
  const totalMin = h * 60 + m;
  // If before 20:00 (i.e., early morning hours), add 24h
  const adj = h < 12 ? totalMin + 1440 : totalMin;
  return adj - TIMELINE_START_HOUR * 60;
}

interface SleepBarProps {
  bedtime: string;
  wakeTime: string;
  durationMin: number;
}

export function SleepBar({ bedtime, wakeTime, durationMin }: SleepBarProps) {
  const startMin = toMinutesSince20(bedtime);
  const leftPct = (startMin / TIMELINE_DURATION_MIN) * 100;
  const widthPct = (durationMin / TIMELINE_DURATION_MIN) * 100;

  const h = Math.floor(durationMin / 60);
  const m = durationMin % 60;
  const label = `${new Date(bedtime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${new Date(wakeTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} (${h}h ${m}m)`;

  return (
    <div>
      <div
        role="img"
        aria-label={`Sleep window: ${label}`}
        title={label}
        style={{ height: 14, background: '#e0f2fe', borderRadius: 4, position: 'relative', overflow: 'hidden' }}
      >
        <div style={{
          position: 'absolute',
          left: `${Math.max(0, Math.min(leftPct, 100))}%`,
          width: `${Math.min(widthPct, 100 - leftPct)}%`,
          height: '100%',
          background: 'var(--primary)',
          borderRadius: 3,
        }} />
      </div>
    </div>
  );
}
```

---

## Sleep debt bar chart

```typescript
interface DebtDay {
  date: string;
  duration_min: number;
  debt_min: number;    // positive = deficit, negative = surplus
}

export function buildDebtChartConfig(days: DebtDay[]): ChartConfiguration<'bar'> {
  return {
    type: 'bar',
    data: {
      labels: days.map(d => d.date),
      datasets: [{
        label: 'Debt (min)',
        data: days.map(d => d.debt_min),
        backgroundColor: days.map(d =>
          d.debt_min <= 0 ? 'rgba(22,163,74,0.5)' :
          d.debt_min <= 60 ? 'rgba(217,119,6,0.5)' :
          'rgba(239,68,68,0.5)'
        ),
        borderWidth: 0,
        borderRadius: 3,
      }],
    },
    options: {
      responsive: true,
      plugins: { legend: { display: false } },
      scales: {
        y: {
          title: { display: true, text: 'Minutes' },
          // positive = deficit, negative = surplus
        },
      },
    },
  };
}
```

---

## Rolling average utility

```typescript
export function rollingAverage(values: number[], window: number): (number | null)[] {
  return values.map((_, i) => {
    if (i < window - 1) return null;
    const slice = values.slice(i - window + 1, i + 1);
    const valid = slice.filter(v => v != null) as number[];
    if (valid.length === 0) return null;
    return valid.reduce((a, b) => a + b, 0) / valid.length;
  });
}
```

Use with `window: 7` for the 7-day rolling average on duration and quality trends.

---

## Bedtime consistency (standard deviation)

```typescript
export function bedtimeConsistency(bedtimes: string[]): {
  stddev_min: number;
  rating: 'Excellent' | 'Good' | 'Fair' | 'Variable';
} {
  const minutes = bedtimes.map(bt => {
    const d = new Date(bt);
    let min = d.getHours() * 60 + d.getMinutes();
    if (d.getHours() < 12) min += 1440; // next-day early hours
    return min;
  });

  const mean = minutes.reduce((a, b) => a + b, 0) / minutes.length;
  const variance = minutes.reduce((sum, m) => sum + (m - mean) ** 2, 0) / minutes.length;
  const stddev = Math.round(Math.sqrt(variance));

  const rating =
    stddev <= 15 ? 'Excellent' :
    stddev <= 30 ? 'Good' :
    stddev <= 60 ? 'Fair' : 'Variable';

  return { stddev_min: stddev, rating };
}
```

---

## Troubleshooting

**StagePie not rendering**
Verify all four stage values are non-null and sum to 100. The component should only be rendered when stage data is present.

**SleepBar overflow**
If `bedtime` is after midnight (0:00-11:59), the component adds 1440 minutes to place it correctly on the 20:00-12:00 timeline. Ensure `bedtime` is an ISO 8601 datetime (not just a time string).

**Rolling average returns all nulls**
`window` (7) is larger than the number of data points. The first `window - 1` values are always null by design. Ensure at least 7 logged entries exist before displaying the rolling average.

**Debt chart shows all green bars**
All nights exceeded the goal. Check that `target_value` in `sleep_goals` for `duration_min` is set correctly.

Related Skills

health-charts

7
from heldernoid/agentic-build-templates

Generate and configure Chart.js health data visualizations including severity timelines, frequency bar charts, trigger correlation charts, and calendar heatmaps. Use when building or modifying charts in any healthcare or wellness tracking app.

sleep-tracker

7
from heldernoid/agentic-build-templates

No description provided.

progress-charts

7
from heldernoid/agentic-build-templates

Recharts-based WPM progress visualization for typing-trainer. Use when implementing or modifying the WPM over time chart, sessions per day bar chart, or any data visualization in the progress dashboard.

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.

Skill: personal-finance

7
from heldernoid/agentic-build-templates

## Overview