@hedia/chart v1.7.0

Chart.js wrapper with Hedia theme support. Showing: light theme. Switch to dark

Color Palette

Categorical Colors

10 colors ordered for maximum contrast between neighbors. Colors adapt to light/dark theme.

blue
green
red
purple
yellow
teal
pink
amber
sky
orange
import { COLORS } from "@hedia/chart/theme";

COLORS.blue      // hsl(233, 80%, 61%)
COLORS.green     // hsl(139, 58%, 40%)
COLORS.red       // hsl(359, 69%, 51%)
COLORS.purple    // hsl(280, 60%, 56%)
COLORS.yellow    // hsl(45, 93%, 58%)
COLORS.teal      // hsl(173, 58%, 44%)
COLORS.pink      // hsl(330, 65%, 54%)
COLORS.amber     // hsl(37, 92%, 52%)
COLORS.sky       // hsl(217, 90%, 65%)
COLORS.orange    // hsl(16, 80%, 55%)

alpha(color, opacity)

Create transparent variants of any color.

1
0.9
0.8
0.7
0.6
0.5
0.4
0.3
0.2
0.1
import { alpha, COLORS } from "@hedia/chart/theme";

alpha(COLORS.blue, 1  )  // hsla(233, 80%, 61%, 1)
alpha(COLORS.blue, 0.9)  // hsla(233, 80%, 61%, 0.9)
alpha(COLORS.blue, 0.8)  // hsla(233, 80%, 61%, 0.8)
alpha(COLORS.blue, 0.7)  // hsla(233, 80%, 61%, 0.7)
alpha(COLORS.blue, 0.6)  // hsla(233, 80%, 61%, 0.6)
alpha(COLORS.blue, 0.5)  // hsla(233, 80%, 61%, 0.5)
alpha(COLORS.blue, 0.4)  // hsla(233, 80%, 61%, 0.4)
alpha(COLORS.blue, 0.3)  // hsla(233, 80%, 61%, 0.3)
alpha(COLORS.blue, 0.2)  // hsla(233, 80%, 61%, 0.2)
alpha(COLORS.blue, 0.1)  // hsla(233, 80%, 61%, 0.1)

Bar Chart

Stacked bar

import { barChart } from "@hedia/chart/bar";

barChart({
  datasets: [
    { data: [80,65,50,35,20,10], label: "3.5.0" },
    { data: [120,150,180,160,140,130], label: "3.6.0" },
    { data: [0,0,40,90,150,200], label: "3.6.1" },
    { data: [0,0,0,0,30,80], label: "3.7.0" },
  ],
  labels: ["Jan","Feb","Mar","Apr","May","Jun"],
  legend: { boxWidth: 12 },
  stacked: true,
  title: "Bolus Calculations by Version",
});

Grouped bar

import { barChart } from "@hedia/chart/bar";
import { COLORS } from "@hedia/chart/theme";

barChart({
  datasets: [
    { backgroundColor: COLORS.green, data: [280,310,350,320,340,390], label: "iOS" },
    { backgroundColor: COLORS.blue, data: [150,170,190,180,200,220], label: "Android" },
  ],
  labels: ["Jan","Feb","Mar","Apr","May","Jun"],
  title: "Calculations by Platform",
});

Horizontal bar

import { barChart } from "@hedia/chart/bar";
import { COLORS } from "@hedia/chart/theme";

barChart({
  datasets: [
    {
      backgroundColor: [COLORS.blue, COLORS.green, COLORS.red, COLORS.purple, COLORS.yellow],
      data: [38,27,20,10,5],
      label: "Usage %",
    },
  ],
  indexAxis: "y",
  labels: ["Food Library","Quick Carbs","Manual Entry","Favourites","Barcode Scan"],
  legend: { display: false },
  title: "Carb Entry Methods",
  y: { callback: (v) => v + "%" },
});

Custom tooltip

Register named callbacks on globalThis.__chartTooltipCallbacks before chart-init.js runs. Reference them by name in the tooltip config.

// 1. Register the callback globally (before chart-init.js)
globalThis.__chartTooltipCallbacks = {
  __TIMELINE_TOOLTIP__: function (context) {
    const events = context.dataset._events;
    if (!events) return context.dataset.label + ": " + context.parsed.y;
    const items = events[context.dataIndex];
    if (!items || items.length === 0) return null;
    return items.map(function (e) { return e.type + " (" + e.time + ")"; });
  },
};

// 2. Use the callback name in the chart config
import { barChart } from "@hedia/chart/bar";

const config = barChart({
  datasets: [{ data: [4, 7, 3, 9, 5, 2], label: "Events" }],
  labels: ["Jan","Feb","Mar","Apr","May","Jun"],
  legend: { display: false },
  title: "Daily Events",
  tooltip: { callbacks: { label: "__TIMELINE_TOOLTIP__" } },
  x: { maxRotation: 0, minRotation: 0 },
  y: { stepSize: 1, title: "Event count" },
});

// 3. Attach per-bar event details for the tooltip
config.data.datasets[0]._events = [
  [{ time: "08:12", type: "calcflow.start" }, { time: "12:45", type: "home.view" }],
  // ... one array per bar
];

Click handler

Register named callbacks on globalThis.__chartClickCallbacks before chart-init.js runs. The callback receives the Chart.js event, elements, and chart arguments. Store pre-computed URLs in a data-hrefs attribute on the canvas — the handler looks up the URL by bar index.

// 1. Register the click callback globally (before chart-init.js)
globalThis.__chartClickCallbacks = {
  __NAVIGATE_ON_CLICK__: function (_event, elements, chart) {
    if (elements.length === 0) return;
    const index = elements[0].index;
    const hrefs = chart.canvas.dataset.hrefs;
    if (!hrefs) return;
    const urls = JSON.parse(hrefs);
    if (urls[index]) globalThis.location.href = urls[index];
  },
};

// 2. Use the callback name in the chart config
import { barChart } from "@hedia/chart/bar";

const config = barChart({
  datasets: [{ data: [163, 142, 141], label: "Logs" }],
  labels: ["id-hedia-com", "oauth2-hedia-com", "developer-hedia-dev"],
  legend: { display: false },
  title: "Logs by Source",
  onClick: { handler: "__NAVIGATE_ON_CLICK__" },
});

// 3. Add pre-computed URLs as a data attribute on the canvas
canvas({
  "data-chart-config": JSON.stringify(config),
  "data-hrefs": JSON.stringify([
    "/logs?source=id-hedia-com",
    "/logs?source=oauth2-hedia-com",
    "/logs?source=developer-hedia-dev",
  ]),
});

Overlapping bar

Two datasets rendered at the same position — foreground on top of background. Ideal for comparing current vs previous period.

import { overlappingBarChart } from "@hedia/chart/bar";
import { alpha, COLORS } from "@hedia/chart/theme";

overlappingBarChart({
  labels: [
    "00", "01", "02", "03", "04", "05", "06", "07",
    "08", "09", "10", "11", "12", "13", "14", "15",
    "16", "17", "18", "19", "20", "21", "22", "23",
  ],
  background: {
    label: "Yesterday",
    data: [3,1,10,12,13,12,15,12,18,14,9,7,11,16,6,5,7,10,15,12,8,5,3,2],
    backgroundColor: alpha(COLORS.red, 0.3),
  },
  foreground: {
    label: "Today",
    data: [2,11,10,9,4,3,7,15,22,17,11,8,14],
    backgroundColor: COLORS.red,
  },
  legend: { boxWidth: 12 },
  title: "Errors: Today vs Yesterday",
});

Line Chart

Area line

import { lineChart } from "@hedia/chart/line";

lineChart({
  datasets: [
    {
      data: [320,410,380,520,490,610],
      fill: true,
      label: "Daily Active Users",
    },
  ],
  labels: ["Jan","Feb","Mar","Apr","May","Jun"],
  legend: { display: false },
  title: "Daily Active Users",
});

Multi-line

import { lineChart } from "@hedia/chart/line";
import { alpha, COLORS } from "@hedia/chart/theme";

lineChart({
  datasets: [
    {
      backgroundColor: alpha(COLORS.red, 0.2),
      borderColor: COLORS.red,
      data: [12,18,9,24,15,8],
      fill: true,
      label: "Errors",
      pointHoverRadius: 6,
      pointRadius: 4,
      tension: 0.5,
    },
    {
      backgroundColor: "transparent",
      borderColor: COLORS.amber,
      borderDash: [5, 3],
      data: [8,12,6,16,10,5],
      label: "Affected users",
      tension: 0,
    },
  ],
  labels: ["Jan","Feb","Mar","Apr","May","Jun"],
  legend: { boxWidth: 12 },
  title: "Error Trend",
});

Doughnut Chart

Carb entry methods

import { doughnutChart } from "@hedia/chart/doughnut";

doughnutChart({
  datasets: [
    {
      data: [40, 30, 20, 10],
    },
  ],
  labels: ["Manual", "Food Library", "Quick Carbs", "Favourites"],
  title: "Carb Entry Methods",
});

Platform split

import { doughnutChart } from "@hedia/chart/doughnut";
import { COLORS } from "@hedia/chart/theme";

doughnutChart({
  datasets: [
    {
      backgroundColor: [COLORS.green, COLORS.blue, COLORS.purple],
      data: [62, 35, 3],
    },
  ],
  labels: ["iOS", "Android", "Unknown"],
  legend: { position: "right" },
  title: "Platform Split",
});

Bubble Chart

Activity heatmap (category scales)

import { bubbleChart } from "@hedia/chart/bubble";
import { alpha, COLORS } from "@hedia/chart/theme";

const HOUR_LABELS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"));
const DAY_LABELS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];

bubbleChart({
  datasets: [
    {
      backgroundColor: alpha(COLORS.teal, 0.6),
      borderColor: COLORS.teal,
      data: [{"x":"07","y":"Mon","r":6},{"x":"08","y":"Mon","r":14},{"x":"09","y":"Mon","r":12},{"x":"12","y":"Mon","r":16},{"x":"13","y":"Mon","r":10},{"x":"17","y":"Mon","r":8},{"x":"18","y":"Mon","r":11},{"x":"20","y":"Mon","r":5},{"x":"08","y":"Tue","r":15},{"x":"09","y":"Tue","r":13},{"x":"12","y":"Tue","r":18},{"x":"13","y":"Tue","r":9},{"x":"17","y":"Tue","r":7},{"x":"19","y":"Tue","r":10},{"x":"08","y":"Wed","r":12},{"x":"09","y":"Wed","r":11},{"x":"12","y":"Wed","r":15},{"x":"17","y":"Wed","r":9},{"x":"20","y":"Wed","r":6},{"x":"10","y":"Thu","r":8},{"x":"12","y":"Thu","r":10},{"x":"14","y":"Thu","r":7},{"x":"11","y":"Fri","r":5},{"x":"13","y":"Fri","r":6}],
      label: "Events",
    },
  ],
  legend: { display: false },
  title: { text: "Hourly Activity (UTC)", padding: { bottom: 25 } },
  xLabels: HOUR_LABELS,
  yLabels: DAY_LABELS,
});

Scatter Chart

Glucose readings

import { scatterChart } from "@hedia/chart/scatter";
import { COLORS } from "@hedia/chart/theme";

scatterChart({
  datasets: [
    {
      data: [{"x":6.2,"y":5.1},{"x":7,"y":6.8},{"x":8.5,"y":7.2},{"x":9.1,"y":8.4},{"x":10.3,"y":9.1},{"x":11,"y":7.5},{"x":12.4,"y":10.2},{"x":13.2,"y":8.8},{"x":14,"y":6.9},{"x":15.5,"y":5.4},{"x":17.1,"y":6.2},{"x":18.8,"y":7.8},{"x":20,"y":9.5},{"x":21.5,"y":8.1}],
      label: "Sensor 1",
      pointHoverRadius: 8,
      pointRadius: 5,
    },
    {
      borderColor: COLORS.amber,
      data: [{"x":6,"y":5.8},{"x":7.3,"y":7.4},{"x":8.1,"y":8},{"x":9.5,"y":9.2},{"x":10.8,"y":8.6},{"x":11.5,"y":7},{"x":12,"y":9.8},{"x":13.8,"y":8.2},{"x":14.5,"y":6.5},{"x":16,"y":5.9},{"x":17.5,"y":7.1},{"x":19.2,"y":8.5},{"x":20.5,"y":10.1},{"x":22,"y":7.6}],
      label: "Sensor 2",
      pointHoverRadius: 8,
      pointRadius: 5,
    },
  ],
  title: "Glucose Readings",
});