Bar, donut and area chart widgets for Angular, built on d3 (the same dependency MJ hosts already declare for ng-graph-view) and colored only through CSS custom properties (var(--token-name)) — MemberJunction design tokens are the expected source. Light/dark themes and org brand overlays re-color the charts through CSS — no re-render, no theme flag.
This is an L1 presentational package ("mjUILayer": "widgets"): data in through inputs, events out through outputs, no data access.
npm install @memberjunction/ng-charts
The components are standalone. Import them into a standalone component or list them in an NgModule's imports.
import { MJBarChartComponent, ChartSeries, ChartPointEvent } from '@memberjunction/ng-charts';
@Component({
imports: [MJBarChartComponent],
template: `
<mj-bar-chart
AriaLabel="Score bands by model"
[Categories]="Models"
[Series]="Bands"
Orientation="horizontal"
[Stacked]="true"
(SegmentClick)="OnSegment($event)">
</mj-bar-chart>
`,
})
export class PortfolioOverview {
Models = ['Churn v3', 'Renewal v1'];
Bands: ChartSeries[] = [
{ Name: 'Healthy', Values: [120, 80], Color: 'var(--mj-status-success)' },
{ Name: 'Critical', Values: [4, 9], Color: 'var(--mj-status-error)' },
];
OnSegment(e: ChartPointEvent): void { /* e.Category, e.SeriesName, e.Value (raw) */ }
}
Orientation: vertical (default) or horizontal. Stacked defaults to false (grouped).
<mj-donut-chart
AriaLabel="Band distribution"
[Slices]="[{ Label: 'Healthy', Value: 120, Color: 'var(--mj-status-success)' }, { Label: 'Critical', Value: 4, Color: 'var(--mj-status-error)' }]"
CenterLabel="Scored"
CenterValue="124"
[ShowPercentLabels]="true"
(SliceClick)="OnSlice($event)">
</mj-donut-chart>
OnSlice(e: ChartSliceEvent): void { /* e.Label, e.Value (raw), e.Percent (0-100) */ }
InnerRadiusRatio defaults to 0.7; 0 draws a pie. Values above 0.85 are clamped. ShowPercentLabels defaults to false. SliceClick emits a ChartSliceEvent.
<mj-area-chart
AriaLabel="Band mix over time"
[Categories]="Weeks"
[Series]="Bands"
StackMode="percent"
Curve="smooth"
(PointClick)="OnPoint($event)">
</mj-area-chart>
OnPoint(e: ChartPointEvent): void { /* e.Category, e.SeriesName, e.Value (raw), e.Percent */ }
StackMode: none (default), stacked, percent. Curve: smooth (default; monotone — never overshoots 0–100%) or linear. FillOpacity defaults to 0.85 and is clamped to 0–1. PointClick emits a ChartPointEvent.
| Input | Default | Notes |
|---|---|---|
AriaLabel |
— | Required. Names the chart for screen readers; missing is logged. |
Height |
300 |
Outer height in px, including legend and axes, or 'fill' (see Sizing). Minimum 120. Width follows the container. |
AspectRatio |
— | Width ÷ height, e.g. 16 / 9. When set it wins over Height (see Sizing). |
ShowLegend |
true |
Wraps to two rows, then "+N more". |
ShowTooltip |
true |
Hides only the visual tooltip; keyboard focus still announces values. |
ValueFormatter |
toLocaleString() |
Receives raw values. Percent axes are formatted by the chart. |
EmptyMessage |
'No data' |
Shown when there is nothing to draw. |
mj-bar-chart and mj-area-chart also take ShowGridlines (default true). false removes the gridlines and keeps the axis labels. Toggling it at runtime shows or hides the gridlines without changing the layout; changing Height or AspectRatio at runtime re-lays the chart out without a resize.
Optional inputs that are null or undefined (Height, AspectRatio, ShowGridlines, FillOpacity, InnerRadiusRatio, StackMode, Curve, Orientation, Stacked, ShowPercentLabels) silently fall back to their defaults, so a binding to a not-yet-loaded value is safe. Only a real out-of-range number is clamped to the nearest limit and logged — once per distinct value, naming the chart by its AriaLabel whatever order the inputs are bound in.
Width always follows the container. Height comes from, in order of precedence (on every path the resolved height is never below 120px):
AspectRatio: the height is width ÷ AspectRatio, so the chart keeps its shape as the container resizes. A smaller ratio is taller for the same width: [AspectRatio]="4 / 3" is taller than [AspectRatio]="16 / 9". Values are clamped to 0.25-8 and logged once; a non-finite or non-positive value, or a string attribute (AspectRatio="1.5"), is logged once and ignored. Bind a number: [AspectRatio]="16 / 9", not a string attribute.Height="fill": the chart takes its parent's height. The parent needs a definite height (a fixed height, a flex or grid track with a size, or height: 100% all the way up). Without one the chart falls back to 300px and logs a hint once.[Height]="240": a fixed px height. The minimum is 120; smaller values are clamped and logged.<mj-bar-chart AriaLabel="Score bands" [Categories]="Models" [Series]="Bands" [AspectRatio]="4 / 3"></mj-bar-chart>
<div style="height: 360px">
<mj-donut-chart AriaLabel="Band distribution" [Slices]="Slices" Height="fill"></mj-donut-chart>
</div>
A chart with AspectRatio inside a scrolling page changes height when its width changes, and a scrollbar appearing or disappearing changes the width. Add scrollbar-gutter: stable to the scroll container so the scrollbar never changes the width; the chart also damps a quick back-and-forth between two widths, but a stable gutter avoids it entirely.
Text sizing is host-driven: the chart reads the font size it computes from --mj-chart-font-size (default --mj-text-xs, 0.75rem), and axes, legend rows, donut labels and the plot box all scale with it.
Every variable is optional; set it on the chart, a wrapper or an ancestor to restyle that subtree. Charts re-color through CSS only, with no re-render.
| Variable | Default | Styles |
|---|---|---|
--mj-chart-font-size |
var(--mj-text-xs, 0.75rem) |
Axis labels, legend, donut labels, tooltip text; the donut center value is 1.67× this |
--mj-chart-grid-color |
var(--mj-border-default) |
Gridlines |
--mj-chart-axis-color |
var(--mj-text-muted) |
Axis labels and empty/invalid state text |
--mj-chart-label-color |
var(--mj-text-secondary) |
Donut center label and percent labels |
--mj-chart-value-color |
var(--mj-text-primary) |
Donut center value |
--mj-chart-legend-color |
var(--mj-text-secondary) |
Legend text, including the "+N more" entry (slightly dimmed) |
--mj-chart-tooltip-bg |
var(--mj-bg-surface-elevated) |
Tooltip background |
--mj-chart-tooltip-color |
var(--mj-text-primary) |
Tooltip text (rows for inactive series are dimmed) |
--mj-chart-tooltip-border |
var(--mj-border-default) |
Tooltip border |
Two caveats:
--mj-chart-tooltip-* (and the tooltip's use of --mj-chart-font-size) apply only when set on an ancestor of the overlay container, that is :root or body. Setting them on the chart or a wrapper restyles everything except the tooltip.For a line chart, use mj-area-chart with [FillOpacity]="0": the series lines are still drawn and the fills are transparent.
<mj-area-chart AriaLabel="Weekly scores" [Categories]="Weeks" [Series]="Scores" [FillOpacity]="0"></mj-area-chart>
Color must be a CSS custom-property reference: var(--token-name). MemberJunction tokens are the expected source. Any other value (hex, a named color, --token without var()) is logged once and replaced by the series' default, var(--mj-viz-N). For series with meaning, use the status tokens (--mj-status-success, --mj-status-info, --mj-status-warning, --mj-status-error); for arbitrary series, leave Color unset.
The host page must define these custom properties (every MJ host does): --mj-viz-1 … --mj-viz-10, --mj-text-primary, --mj-text-secondary, --mj-text-muted, --mj-border-default, --mj-bg-surface-elevated, --mj-focus-ring-color, --mj-radius-sm, --mj-radius-md, --mj-shadow-md, plus any status tokens you pass.
| Case | Bar | Area none |
Area stacked / percent |
Donut |
|---|---|---|---|---|
| No categories / series / slices | empty state | empty state | empty state | empty state |
Values.length ≠ Categories.length |
invalid state + logged | invalid | invalid | — |
All values 0 |
axes + baseline; each 0 can still be hovered, focused and clicked |
flat at 0 | flat at 0 | empty state |
null, NaN, ±Infinity |
no bar; "—" in tooltip/table | gap in the line | counted as 0, drawn continuous | slice skipped |
| A value with no neighbour on either side (between two gaps, or a single category) | — | drawn as a dot | drawn as a dot | — |
| Negative values | grouped: below 0; stacked: away from 0 | allowed | invalid + logged | slice excluded + logged |
Inputs are snapshots. Mutating an array in place does not update the chart — pass a new array.
Events carry the caller's raw value, except that non-finite values (NaN, ±Infinity) are reported as null. In percent mode ChartPointEvent.Percent adds the 0–100 share.
Documented soft caps: bar ≤ 200 categories × 10 series; area ≤ 2,000 categories × 10 series; donut ≤ 50 slices. Above a cap the chart still renders and logs once; the screen-reader table lists the first 500 rows. Aggregate or decimate before charting.
Each chart is one tab stop (role="group"). Arrow keys move between values, Home/End jump to the ends, Enter/Space activates. Escape closes the tooltip — from anywhere on the page, for as long as the tooltip is open. A visually hidden data table carries every value for screen readers, up to the first 500 rows; the legend is text plus a swatch.