Skip to content

Chart.js โ€‹

Charts built on Chart.js. A controlMonoChart(...) controller projects your rows into chart data โ€” the same controller-plus-.prop shape as controlMonoTable โ€” and the <mono-chart-*> elements own the canvas.

ts
import 'mono-helper/ui/chart'
import { controlMonoChart } from 'mono-helper'

const chart = controlMonoChart(rows, {
  labelField: 'month',
  series: [
    { field: 'sales', label: 'Sales' },
    { field: 'costs', label: 'Costs' },
  ],
})
vue
<mono-chart-bar :control-chart.prop="chart" height="320" title="Sales vs costs" />

Install

Chart.js is an optional peer โ€” it isn't bundled, so nothing pays for it unless you chart something.

sh
pnpm add chart.js@4.5.1

The version is pinned exact. If it's missing, the element renders an inline message naming the package and the command rather than failing silently.

Elements โ€‹

ElementType
<mono-chart>whatever type says โ€” radar, polarArea, scatter, bubble โ€ฆ
<mono-chart-bar>bar
<mono-chart-line>line
<mono-chart-pie>pie
<mono-chart-doughnut>doughnut

The presets are <mono-chart> with type locked, so the markup states its intent. Everything else โ€” props, controller binding, lifecycle โ€” is identical.

Basic โ€‹

labelField names the category axis; each series entry becomes one dataset. Field names accept the same path expressions as the table (Job.Budget, Items.[0].Total).

Chart types โ€‹

One controller can feed several elements โ€” the projection is shared, only the type differs.

Aggregating rows โ€‹

Charts almost always want summarised data, but a data source hands you raw rows. groupBy buckets them and each series' agg collapses a bucket to one number โ€” sum (default), avg, count, min or max.

ts
const chart = controlMonoChart(transactions, {
  groupBy: 'region',
  series: [{ field: 'amount', label: 'Revenue', agg: 'sum' }],
})

Reactive data โ€‹

The controller is a subscribable store, exactly like controlMonoTable. When the bound source emits changed, it re-syncs, re-projects and notifies โ€” and the element updates the existing Chart.js instance instead of recreating the canvas, so the change animates rather than flashing.

Notifications are coalesced on a microtask, so several mutations in one tick produce a single update.

From a DataSource โ€‹

Bind a devextreme DataSource and the controller drains it โ€” a remote source is paged, but a chart wants the whole set, so it reads every row in chunks rather than charting page one. Set loadAll: false to chart just the current page.

Server-side roll-up ($apply) โ€‹

Draining is the wrong shape for a summary chart: a department-total over 100k rows is ~1000 paged requests carrying every column, to produce a dozen bars. Point the controller at the endpoint with odata and give it an aggregate โ€” the server groups with OData's $apply and returns one row per bucket, in a single request. A filter folds inside the clause, so it's applied before the aggregation.

Requires the mono-utils peer

The request goes through monoOdataFetch. It's an optional peer loaded on demand โ€” a chart that never sets odata neither needs nor loads it.

Raw chart.js data โ€‹

Skip the mapping entirely: hand the controller a chart.js { labels, datasets } object and it's used verbatim. A per-dataset type gives you mixed charts.

Theme โ€‹

Charts are wired to the theme like every other component. The default palette is mono color names, not fixed hex, so a chart with no color or colors still follows the active color preset โ€” and so do its axes, grid, legend and title.

A canvas doesn't re-cascade

Switching theme repaints every other component for free: the class swap re-cascades and CSS redraws. A chart can't โ€” it's a bitmap painted from values resolved once, so it would keep the old palette. The elements listen for the theme-changed event applyTheme() fires and redraw themselves. If you swap the theme classes by hand instead of calling applyTheme(), dispatch that event yourself:

ts
window.dispatchEvent(new CustomEvent('theme-changed'))

Colors โ€‹

Anywhere a colour is accepted you can use a mono color name โ€” primary, secondary, accent, success, warning, danger, info โ€” or any CSS colour, mixed freely. Use color for one accent, colors for a palette, or series[].color per dataset; the more specific one wins.

Why names have to be resolved

A <canvas> can't resolve var() โ€” raw var(--theme-success) paints transparent. The element reads the token off the DOM and hands Chart.js the real value, which is why names work at all.

Customising โ€‹

Every layer is live in one demo: color / colors on the element, series[].color per dataset, --mono-chart-* to retarget what a name means, and raw Chart.js options for everything else.

Anything Chart.js supports is reachable through options on the controller or chart-options on the element โ€” both are deep-merged over the generated defaults, so you keep the theming and override only what you name.

Overriding the variable is the theme-level lever โ€” every chart that asked for success follows, with no chart code touched:

css
.sales-dashboard {
  --mono-chart-success: #0f9d58;
  --mono-chart-danger: #d93025;
}

Controller API โ€‹

ts
const chart = controlMonoChart(source, options)

source is an array, a devextreme DataSource, a raw chart.js data object, or null.

OptionPurpose
typeDefault chart type ('bar')
labelFieldRow field for the category axis
groupByBucket rows by this field before charting
series[]{ field, label?, agg?, color?, type?, dataset? } โ€” one dataset each
colors[]Palette โ€” mono color names or CSS colors
optionsRaw chart.js options, deep-merged
propsThe element's own props, declared centrally โ€” see Element props
odataFetch through monoOdataFetch (configBaseUrl / baseUrl / url / method / options). Add aggregate to roll up on the server โ€” see Server-side roll-up
loadAllDrain a remote source instead of charting one page (default true)
MemberPurpose
itemsRows currently backing the chart
loadingTrue while the source loads
data() / options()The projected chart.js data / merged options
props() / setProps(patch)Read / merge the central element props
setType / setSeries / setColors / setDataReproject
bind(source) / reload()Swap or re-read the source
onPointClickAssignable sink โ€” receives the label, value and the rows that produced the point
subscribe(cb) / dispose()Lifecycle

onPointClick reports the underlying rows, which is what makes drill-down work: click a bar and you get back the bucket that built it.

ts
chart.onPointClick = ({ label, value, rows }) => {
  console.log(label, value, rows) // rows = every record in that bucket
}

Element props โ€‹

Declare the element's own props in the controller's props block and every bound <mono-chart*> needs nothing but :control-chart.prop="chart" โ€” the same arrangement as controlMonoTable({ props }), minus the per-element nesting, since the presets are one element with type fixed.

ts
const chart = controlMonoChart(rows, {
  groupBy: 'month',
  series: [{ field: 'amount', label: 'Revenue', agg: 'sum' }],
  props: { height: 320, legend: 'bottom', stacked: true },
})

chart.props()                      // { height: 320, legend: 'bottom', stacked: true }
chart.setProps({ legend: 'right' }) // merges, notifies, every bound element re-applies

props() returns one object with a stable identity, so a controller can drive several charts and keep them in sync. setProps is a merge โ€” keys you omit keep their current value.

Setting props on the element still works

Writing height / legend / stacked directly on <mono-chart-bar> is still supported. Where both declare the same key, the controller wins; keys the controller never mentions are left to the template.

Server rendering โ€‹

A chart can't be server-rendered โ€” Chart.js paints onto a canvas at runtime. The SSR build (mono-helper/ui/shadow/chart) emits a correctly-sized empty <canvas> and draws on hydration, so layout doesn't shift. controlMonoChart itself is a pure factory and is safe to import on the server.

Props โ€‹

controlMonoChart is also exported as monoChart, and :control-chart is also accepted as :data-chart โ€” the older spellings still work.

Importimport { ChartProps } from 'mono-helper'
PropValueDefaultDescription
controlChartMonoChartController<any>โ€”The chart controller. Bind with `.prop`: `:control-chart.prop="chart"`.
dataChartMonoChartController<any>โ€”Renamed โ€” `:data-chart` / `:dataChart` alias `controlChart` (both work).
type'bar' | 'line' | 'pie' | 'doughnut' | 'radar' | 'polarArea' | 'scatter' | 'bubble'โ€”Chart type. Ignored by the presets, which fix their own. When a controller is bound this overrides the controller's `type`.
dataMonoChartDataโ€”Raw chart.js data, for use WITHOUT a controller. Bind with `.prop`: `:data.prop="{ labels, datasets }"`.
chartOptionsRecord<string, unknown>โ€”Raw chart.js options, merged over the defaults. Bind with `.prop`.
widthstring | numberโ€”Canvas box sizing. A CSS length string or a number (px).
heightstring | numberโ€”โ€”
aspectRationumberโ€”`width / height` when no explicit height is set. Default `2`.
legend'left' | 'right' | 'top' | 'bottom' | booleanโ€”Show the legend (default `true`), or place it: `top` / `bottom` / `left` / `right`.
titlestringโ€”Chart title rendered by chart.js.
stackedbooleanโ€”Stack bar/line datasets. Default `false`.
colorstringโ€”Single accent for every dataset โ€” a mono color name (`primary`, `success`, `danger`, โ€ฆ) or any CSS color. `colors` wins when both are set.
colorsstring | string[]โ€”Palette. A comma-separated attribute (`colors="success,danger,warning"`) or an array via `.prop`. Entries may be mono color names or CSS colors, mixed.
cssClassChartCssClassโ€”Per-part class overrides. Object, or a JSON string via the `css-class` attribute.
cssClassNamestringโ€”A single class added to the root.

Optional dependency โ€‹

chart.js is declared as an optional peerDependency pinned to 4.5.1, and is loaded with a dynamic import('chart.js/auto') the first time an element renders. It is externalized from the build, so dist/ui/chart.js stays around 13 kB and resolves against your app's copy at runtime โ€” install it yourself, and nothing else in mono-helper is affected if you don't.