Chart.js Integration

Charts use Chart.js with a set of opinionated defaults that align them with the design system's visual language.

The following adjustments are applied on top of the default Chart.js configuration:

  • Tooltips are off.

    plugins.tooltip.enabled: false

  • Animation is off.

    animation: false at the options level.

  • One bar / label per time unit. For example, data arriving at sub-day granularity is aggregated by day before rendering.
  • Bars fill the full category width with a small gap between them.

    categoryPercentage: 1, barPercentage: 0.92 — set at the options level.

  • All colors come from CSS tokens.

    Chart-specific palette tokens use the --m__chart-* prefix.

    Grid and tick colors are read from --m__text and --m__text-caption via getComputedStyle. Grid color is derived as color-mix(in srgb, --m__text 12%, transparent). Palette tokens are defined in tokens/colors.css.

  • Theme-aware re-rendering. When the theme class on the body element changes, all charts are destroyed and re-created with fresh colors from CSS variables.

    A MutationObserver watches the class attribute on <body>.

  • Legend at the bottom with round 8×8 markers shown only when there is more than one dataset.

    position: "bottom", labels.usePointStyle: true, labels.pointStyle: "circle", labels.boxWidth: 8, labels.boxHeight: 8.

    plugins.legend.display: false for single-dataset charts.

  • X-axis labels stay horizontal. At narrow widths labels are auto-skipped rather than rotated.

    ticks.maxRotation: 0

  • Smart X-axis labels. The first label on the axis and the first label of each new month include the abbreviated month name; all other labels show only the day number.

    The formatter tracks the current month while iterating through sorted dates. On month change (or at index 0) it outputs toLocaleDateString("en-US", { month: "short", day: "numeric" }); otherwise — d.getDate().

  • Gradient fill when the axis does not start at zero. The filled area fades to transparent in the bottom 1/3.

    A CanvasGradient runs from chartArea.top to chartArea.bottom with stops at 0 → rgba(…, 0.35), 0.67 → rgba(…, 0.35), 1 → rgba(…, 0). Base color: --m__chart-default. Y-axis uses grace: "25%".

  • Hover interaction. Moving the cursor over a chart activates a vertical index zone spanning the full chart height — the active position corresponds to a single day, not to individual shapes or points. On line charts a dot appears at the data point; on bar charts the hovered column is brightened.

    pointHoverRadius: 2.5 with pointRadius: 0 on the dataset; hover color is rgbToRgba(baseRgb, 0.9).

    interaction: { mode: "index", intersect: false } on all charts. Bar hover color uses hoverBackgroundColor: lightenColor(color, 0.35) — mixes the base color with white to brighten consistently in both light and dark themes.

  • Status bar on line charts. While hovering, a reserved single line below the chart shows the date and value for that position. The line is empty when the cursor is outside the chart area.

    A <p class="chart-status-bar"> element sits below the chart container; its innerHTML is replaced on mousemove and cleared on mouseleave. Values are read with chart.getElementsAtEventForMode(e, "index", { intersect: false }). min-height: 1.3em reserves the space so the widget height does not change.

  • Merged legend and status bar on bar charts. The legend below the chart doubles as a hover status bar: by default it shows colored dots with dataset labels; while hovering it transforms to show the date followed by each label with its colored value.

    A <div class="chart-legend"> element is populated with innerHTML on every render and updated on mousemove / mouseleave. Chart.js built-in legend is disabled (plugins.legend.display: false).