📊 Chart Integration #
Complete guide to integrating and customizing charts in Admindek using Chart.js and the shared AdmindekChart preset.
Overview #
Admindek uses Chart.js 4 for every chart in the template: dashboards, widgets, the Chart.js showcase page and the sparklines. Chart.js is MIT licensed, and so are the three plugins the showcase page adds (matrix, financial and zoom).
On top of Chart.js sits one small file, src/assets/js/chart-theme.js. It:
- Sets
Chart.defaultsfrom the template's CSS variables (font, text, grid, surface and palette colours) - Repaints every chart when the customizer switches dark/light mode, RTL/LTR or the preset colour
- Exposes
window.AdmindekChartwith helpers:create(),radial(),sparkline(),gradient(),alpha(),color(),palette(),refresh()andmerge() - Registers two tiny plugins:
centerText(label in the middle of a doughnut) andvalueLabels(values on bars and slices)
| Page | Menu | Script |
|---|---|---|
chart/chart-chartjs.html | Charts → Chart.js | src/assets/js/chart_maps/chart-chartjs.js |
chart/chart-peity.html | Charts → Sparkline Chart | src/assets/js/chart_maps/chart-sparkline.js |
See both pages in the live demo: Chart.js showcase and Sparklines. The old chart/chart-apex.html URL redirects to chart/chart-chartjs.html.
How Charts Are Loaded #
Chart.js is not bundled by Vite. It is loaded as a plain <script> on the pages that need it, in this order:
<!-- 1. Chart.js -->
<script src="../assets/js/plugins/chart.umd.min.js"></script>
<!-- 2. The Admindek theme preset (defines window.AdmindekChart) -->
<script src="../assets/js/chart-theme.js"></script>
<!-- 3. The page's chart script -->
<script src="../assets/js/widgets/bar-chart.js"></script>Pages in the root folder (such as index.html) use assets/js/... without the ../.
The showcase page chart/chart-chartjs.html also loads the three MIT plugins, after Chart.js and before chart-theme.js:
<script src="../assets/js/plugins/chart.umd.min.js"></script>
<script src="../assets/js/plugins/chartjs-chart-matrix.min.js"></script> <!-- heatmaps -->
<script src="../assets/js/plugins/chartjs-chart-financial.min.js"></script> <!-- candlestick -->
<script src="../assets/js/plugins/chartjs-plugin-zoom.min.js"></script> <!-- zoom -->
<script src="../assets/js/chart-theme.js"></script>
<script src="../assets/js/chart_maps/chart-chartjs.js"></script>The vendor files are copied from node_modules into dist/assets/js/plugins/ by plugins/vite-plugin-copy-assets.js:
| Package | File | Size |
|---|---|---|
chart.js | chart.umd.min.js | ~204 KB minified, ~69 KB gzipped |
chartjs-chart-matrix | chartjs-chart-matrix.min.js | ~4 KB |
chartjs-chart-financial | chartjs-chart-financial.min.js | ~12 KB |
chartjs-plugin-zoom | chartjs-plugin-zoom.min.js | ~15 KB |
Basic Chart Setup #
1. HTML Container #
Charts render into an ordinary <div> with an id. AdmindekChart.create() adds the <canvas> for you.
<div class="card">
<div class="card-header">
<h5 class="mb-0">Sales Analytics</h5>
</div>
<div class="card-body">
<div id="bar-chart"></div>
</div>
</div>2. JavaScript Configuration #
// src/assets/js/widgets/bar-chart.js
'use strict';
document.addEventListener('DOMContentLoaded', function () {
setTimeout(function () {
var from = ['#1de9b6', '#a389d4', '#04a9f5', '#f44236'];
var to = ['#1dc4e9', '#899ed4', '#049df5', '#f48f36'];
AdmindekChart.create(
'#bar-chart',
{
type: 'bar',
data: {
labels: ['Sport', 'Music', 'Travel', 'News'],
datasets: [
{
label: 'News',
data: [53, 13, 30, 4],
// one two-colour gradient per bar
backgroundColor: function (ctx) {
return AdmindekChart.gradient([to[ctx.dataIndex], from[ctx.dataIndex]])(ctx);
},
categoryPercentage: 0.7,
barPercentage: 0.5
}
]
},
options: {
plugins: {
legend: { display: false },
tooltip: {
displayColors: false,
callbacks: {
title: function () {
return '';
}
}
}
},
scales: {
y: { display: false, beginAtZero: true }
}
}
},
{ height: 250 }
);
}, 500);
});This is a simplified version of the bar chart in src/assets/js/widgets/bar-chart.js (the shipped file computes each gradient across the bar itself rather than the whole chart area). The config is a standard Chart.js configuration (type, data, options). The third argument is Admindek-specific: it sizes the container.
The AdmindekChart API #
| Helper | Returns | Purpose |
|---|---|---|
create(target, config, opts) | Chart or null | Create a chart inside a container |
radial(target, options) | Chart or null | Radial progress / multi-ring progress (a doughnut per ring) |
sparkline(extra) | options object | Options for axis-less, legend-less, tooltip-less mini charts |
gradient(colors, opts) | scriptable colour | Canvas gradient across the chart area |
alpha(color, a) | rgba() string | Add transparency to a colour or theme token |
color(name) | string | Current value of a theme token ('primary', 'text', 'track' ...) |
palette() | array | Default series colours, starting with the active preset colour |
refresh() | – | Re-read the theme and repaint every chart |
merge(target, source) | object | Chart.helpers.merge (deep merge of option objects) |
create(target, config, opts) #
const chart = AdmindekChart.create('#revenue-chart', config, { height: 300 });target: a selector, an element, or a<canvas>. Returnsnullif the element does not exist, so a missing container never throws.config: a normal Chart.js config.opts.height: number (px) or CSS length. Sets the container's height and turns offmaintainAspectRatio(unless you set it yourself). Without a height, Chart.js keeps its default aspect ratio.opts.width: number (px) or CSS length. Fixes the container's width, useful for tiny inline charts; an inline container becomesinline-block.opts.label: accessible label for the canvas. The container's ownaria-labelattribute is used if present.
The canvas gets class="admindek-chart" and role="img". Calling create() again on the same container destroys the previous chart first, so re-rendering is safe.
gradient(colors, opts) #
Returns a scriptable colour, so pass it directly as backgroundColor or borderColor:
AdmindekChart.gradient('#4680ff'); // vertical fade, 0.4 -> 0 opacity (area fills)
AdmindekChart.gradient('#4680ff', { from: 0.6, to: 0.05 }); // custom opacities
AdmindekChart.gradient(['#1de9b6', '#1dc4e9']); // two-colour gradient (bars, rings)
AdmindekChart.gradient(['#1dc4e9', '#1de9b6'], { direction: 'horizontal' });
AdmindekChart.gradient('primary'); // theme token, re-read on preset changealpha(color, a), color(name) and palette() #
AdmindekChart.alpha('#4680ff', 0.2); // 'rgba(70, 128, 255, 0.2)'
AdmindekChart.alpha('primary', 0.2); // works with theme tokens too
AdmindekChart.color('primary'); // current --bs-primary, e.g. '#4680ff'
AdmindekChart.color('#ff0000'); // unknown names are returned unchanged
AdmindekChart.palette(); // [primary, success, warning, danger, indigo, info, orange, pink]alpha() accepts #rgb, #rrggbb, rgb() and rgba() values.
Theme tokens available to color(), alpha() and gradient():
| Token | Source |
|---|---|
primary, secondary, success, danger, warning | --bs-primary ... --bs-warning |
info, indigo, teal, orange, pink, purple | --bs-cyan, --bs-indigo, --bs-teal, --bs-orange, --bs-pink, --bs-purple |
text, heading | --bs-body-color, --pc-heading-color |
grid, border | Grid line colour, --bs-border-color |
surface | The card background (used for slice borders) |
track | Empty part of progress rings |
tooltipBg, tooltipText, tooltipBorder | Tooltip colours |
font | --bs-body-font-family |
dark, rtl | Booleans: current mode and direction |
sparkline(extra) #
Returns options with axes, legend and tooltip hidden, no points and a 2px line. Pass extra options to deep-merge them in:
// Plain sparkline
{ type: 'line', data: data, options: AdmindekChart.sparkline() }
// Sparkline with the tooltip turned back on
{ type: 'line', data: data, options: AdmindekChart.sparkline({ plugins: { tooltip: { enabled: true } } }) }radial(target, options) #
Builds radial progress bars (a doughnut per ring with a background track) and calls create() for you.
AdmindekChart.radial('#goal', {
values: [76], // one number per ring, outer ring first
labels: ['Completed'],
colors: ['success'], // theme tokens, colours or gradient() results
height: 250,
text: '76%', // centre label (string or function(chart))
sub: 'Completed' // smaller second line
});| Option | Default | Description |
|---|---|---|
values | [] | One value per ring (outer ring first) |
max | 100 | Value that fills a ring |
colors | palette() | Ring colours: tokens, colour strings or gradient() results |
labels | Series 1... | Ring names, used in tooltips |
height, width | – | Passed to create() |
cutout | '72%' (one ring), '35%' (several) | Size of the hole |
circumference | 360 | Degrees to sweep; 180 for a half gauge |
rotation | 0 | Start angle; -90 with circumference: 180 for a half gauge |
text, sub | – | Centre label and sub-label (string or function(chart)) |
textSize, textColor | auto, heading colour | Centre label styling |
rounded | true | Rounded ring ends |
trackColor | 'track' token | Colour of the unfilled part |
padding | 4 | Layout padding |
tooltip | true | false disables the tooltip |
suffix | '%' | Appended to the value in the tooltip |
label | – | Accessible label for the canvas |
options | – | Extra Chart.js options, deep-merged |
refresh() #
Re-reads the CSS variables and repaints every chart. It runs automatically on theme changes (see Dark Mode, RTL and Preset Colours); call it yourself only if you change CSS variables some other way.
Built-in Plugins #
chart-theme.js registers two small plugins globally. They are off unless you configure them.
centerText #
Draws a label in the middle of a doughnut or pie:
options: {
plugins: {
centerText: {
text: '12.4K', // string or function (chart) => string
sub: 'Total Visits', // optional second line
color: null, // defaults to the heading colour
subColor: null, // defaults to the text colour
size: null, // font size in px, auto-sized when null
weight: '600'
}
}
}valueLabels #
Draws values on bars and slices:
options: {
plugins: {
valueLabels: {
display: true,
color: '#ffffff', // or function ({ datasetIndex, dataIndex }) => colour
size: 11,
// Optional. Default: the raw value on bars, the percentage of the
// visible total on pie, doughnut and polar area charts.
formatter: function (value, ctx) {
return value + ' (' + Math.round((value / ctx.total) * 100) + '%)';
}
}
}
}Return '' or null from formatter to skip a label.
What the Theme Preset Sets #
You do not need to repeat these in every chart:
- Fonts and colours: body font at 12px, text and grid colours from the theme
- Scales: no axis border or tick marks, subtle horizontal grid lines, no vertical grid on category axes
- Lines:
tension: 0.4, 2px width, points hidden until hover - Bars: 4px border radius
- Doughnut/pie slices: 2px border in the card colour, small hover offset
- Legend: bottom, circular point markers
- Tooltips: theme colours, rounded, point-style markers; line and bar charts use shared (
index) tooltips - Animation: 800ms,
easeOutQuart
Any option you set in a chart's config overrides the preset for that chart.
Chart Types #
Line and Area Chart (with Gradient) #
An area chart is a line chart with fill: true:
AdmindekChart.create(
'#revenue-chart',
{
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'],
datasets: [
{
label: 'Revenue',
data: [31, 40, 28, 51, 42, 109, 100],
borderColor: '#4680ff',
backgroundColor: AdmindekChart.gradient('#4680ff', { from: 0.35, to: 0.05 }),
fill: true
},
{
label: 'Profit',
data: [11, 32, 45, 32, 34, 52, 41],
borderColor: '#2ecc71',
backgroundColor: AdmindekChart.gradient('#2ecc71', { from: 0.35, to: 0.05 }),
fill: true
}
]
},
options: {
plugins: { legend: { position: 'top', align: 'end' } }
}
},
{ height: 300 }
);For a plain line chart, leave out fill and backgroundColor. Use tension: 0 for straight segments and borderDash: [5, 5] for a dashed series (the dashboard's forecast line does this).
Bar Chart #
AdmindekChart.create(
'#products-chart',
{
type: 'bar',
data: {
labels: ['Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q6'],
datasets: [{ label: 'Products Sold', data: [44, 55, 57, 56, 61, 58], backgroundColor: '#4680ff' }]
},
options: {
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } }
}
},
{ height: 350 }
);Stacked bars: stack both axes.
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
datasets: [
{ label: 'Online', data: [12, 19, 14, 17, 22], backgroundColor: '#4680ff' },
{ label: 'In store', data: [8, 11, 9, 12, 10], backgroundColor: '#2ecc71' }
]
},
options: {
scales: { x: { stacked: true }, y: { stacked: true } }
}Horizontal bars: set indexAxis: 'y'.
options: {
indexAxis: 'y',
plugins: { valueLabels: { display: true } } // values inside the bars
}Doughnut and Pie Chart #
AdmindekChart.create(
'#device-chart',
{
type: 'doughnut', // or 'pie'
data: {
labels: ['Desktop', 'Mobile', 'Tablet', 'Smart TV', 'Others'],
datasets: [{ data: [44, 55, 41, 17, 15], backgroundColor: AdmindekChart.palette().slice(0, 5) }]
},
options: {
cutout: '70%',
plugins: {
centerText: {
text: function (chart) {
return chart.data.datasets[0].data.reduce(function (a, b) {
return a + b;
}, 0);
},
sub: 'Total'
}
}
}
},
{ height: 300 }
);A simple "value vs remainder" ring (as in src/assets/js/widgets/happy-sad-ball.js) colours the second slice with the track token:
backgroundColor: function (ctx) {
return ctx.dataIndex === 0 ? '#1de9b6' : AdmindekChart.color('track');
}Radial Progress #
// Single ring with a gradient (src/assets/js/widgets/chart-sale.js)
AdmindekChart.radial('#chart-sale', {
values: [85],
labels: ['Sales'],
colors: [AdmindekChart.gradient(['#1de9b6', '#1dc4e9'], { direction: 'horizontal' })],
height: 220,
cutout: '89%',
text: '85',
textSize: 36
});
// Several concentric rings
AdmindekChart.radial('#goals', {
values: [76, 67, 61],
labels: ['Sales', 'Leads', 'Visits'],
colors: ['primary', 'success', 'warning'],
height: 260
});
// Half gauge
AdmindekChart.radial('#uptime', {
values: [99.9],
labels: ['Uptime'],
circumference: 180,
rotation: -90,
height: 180,
text: '99.9%'
});Sparkline #
Small trend charts in KPI cards (from the dashboard in src/html/index.html):
AdmindekChart.create(
'#total-revenue-chart',
{
type: 'line',
data: {
labels: ['', '', '', '', '', '', ''],
datasets: [{ data: [31, 40, 28, 51, 42, 85, 77], borderColor: '#ffffff', borderWidth: 2, tension: 0 }]
},
options: AdmindekChart.sparkline()
},
{ width: 80, height: 50 }
);Bar sparklines work the same way with type: 'bar'. Chart.js needs a labels entry per value, so use empty strings when there are no category names.
Updating Data Live #
Keep the instance returned by create(), change its data and call update():
const live = AdmindekChart.create(
'#realtime-chart',
{
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Live Data',
data: [],
borderColor: () => AdmindekChart.color('primary'),
backgroundColor: AdmindekChart.gradient('primary'),
fill: true
}
]
},
options: { scales: { y: { min: 0, max: 100 } } }
},
{ height: 350 }
);
setInterval(function () {
live.data.labels.push(new Date().toLocaleTimeString());
live.data.datasets[0].data.push(Math.floor(Math.random() * 100));
// keep the last 20 points
if (live.data.labels.length > 20) {
live.data.labels.shift();
live.data.datasets[0].data.shift();
}
live.update();
}, 2000);- Replace a whole series:
chart.data.datasets[0].data = newData; chart.update(); - Skip the animation:
chart.update('none'); - Show or hide a series:
chart.setDatasetVisibility(1, false); chart.update();
Loading Data from an API #
async function createApiChart() {
const response = await fetch('/api/sales-data');
if (!response.ok) return;
const data = await response.json(); // { labels: [...], values: [...] }
AdmindekChart.create(
'#api-chart',
{
type: 'line',
data: {
labels: data.labels,
datasets: [{ label: 'Sales', data: data.values, borderColor: '#4680ff' }]
}
},
{ height: 350 }
);
}Dark Mode, RTL and Preset Colours #
Charts follow the customizer without any extra code. chart-theme.js watches the data-pc-theme, data-pc-direction and data-pc-preset attributes on <body> and the dir attribute on <html> with a MutationObserver. When one changes, it re-reads the CSS variables, updates Chart.defaults and repaints every chart on the page.
What updates automatically:
- Text, grid, tooltip and legend colours, and slice borders (they come from the preset defaults)
- Anything given as a theme token to
gradient(),alpha()orradial(), e.g.gradient('primary') - Scriptable colours that call
AdmindekChart.color()when drawing, e.g.borderColor: () => AdmindekChart.color('primary')
What stays fixed:
- Hard-coded colours such as
'#4680ff' - A value read once when the chart is created, e.g.
borderColor: AdmindekChart.color('primary'). Wrap it in a function to make it follow the preset colour.
RTL: legend and tooltip text switch to right-to-left. Axes are not mirrored; set reverse: true on the x scale if you want the category order flipped in RTL layouts.
Charts inside Bootstrap tabs, modals, collapses, offcanvas panels and dropdowns are resized when those are shown, so they do not stay at zero width.
Adding the Matrix, Financial and Zoom Plugins #
The three plugins are copied to assets/js/plugins/ by the build, but only chart/chart-chartjs.html loads them. To use one on another page, add its script after chart.umd.min.js and before chart-theme.js:
<script src="../assets/js/plugins/chart.umd.min.js"></script>
<script src="../assets/js/plugins/chartjs-chart-matrix.min.js"></script>
<script src="../assets/js/chart-theme.js"></script>The UMD builds register themselves with Chart.js, so no Chart.register() call is needed.
Heatmap (chartjs-chart-matrix) #
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
const hours = ['9am', '12pm', '3pm', '6pm'];
const cells = [];
days.forEach((day) => hours.forEach((hour) => cells.push({ x: hour, y: day, v: Math.round(Math.random() * 100) })));
AdmindekChart.create(
'#heatmap',
{
type: 'matrix',
data: {
datasets: [
{
label: 'Visits',
data: cells,
backgroundColor: (ctx) => AdmindekChart.alpha('primary', 0.15 + (((ctx.raw && ctx.raw.v) || 0) / 100) * 0.85),
borderColor: () => AdmindekChart.color('surface'),
borderWidth: 2,
width: ({ chart }) => (chart.chartArea || {}).width / hours.length - 2,
height: ({ chart }) => (chart.chartArea || {}).height / days.length - 2
}
]
},
options: {
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: () => '',
label: (item) => item.raw.y + ' ' + item.raw.x + ': ' + item.raw.v
}
}
},
scales: {
x: { type: 'category', labels: hours, offset: true, grid: { display: false } },
y: { type: 'category', labels: days, offset: true, grid: { display: false } }
}
}
},
{ height: 300 }
);Candlestick (chartjs-chart-financial) #
The plugin adds the candlestick and ohlc chart types. Each point is { x, o, h, l, c }:
AdmindekChart.create(
'#candlestick',
{
type: 'candlestick',
data: {
datasets: [
{
label: 'ACME',
data: [
{ x: 1, o: 6629, h: 6650, l: 6623, c: 6633 },
{ x: 2, o: 6632, h: 6643, l: 6620, c: 6630 },
{ x: 3, o: 6630, h: 6648, l: 6618, c: 6640 }
]
}
]
},
options: {
plugins: { legend: { display: false } },
scales: { x: { type: 'linear', ticks: { stepSize: 1 } } }
}
},
{ height: 350 }
);By default the plugin puts a timeseries axis on x, which needs a Chart.js date adapter. The template does not ship one, so either use a linear or category x axis as above (with numbers or your own tick labels), or add an MIT date adapter such as chartjs-adapter-date-fns before chart-theme.js and use real timestamps.
Zoom (chartjs-plugin-zoom) #
const chart = AdmindekChart.create(
'#zoom-chart',
{
type: 'line',
data: { labels: labels, datasets: [{ label: 'Visitors', data: values, borderColor: '#4680ff' }] },
options: {
plugins: {
zoom: {
zoom: {
wheel: { enabled: true }, // mouse wheel
drag: { enabled: true }, // drag a box to zoom
mode: 'x'
}
}
}
}
},
{ height: 350 }
);
// Reset from a button
document.querySelector('#reset-zoom').addEventListener('click', function () {
chart.resetZoom();
});Wheel and drag zoom work as-is. Panning and pinch-zoom need Hammer.js, which the template does not copy to assets/js/plugins/; add it before the zoom plugin if you need them.
Destroying and Recreating Charts #
// Recreate: create() on the same container destroys the old chart first
AdmindekChart.create('#sales-chart', monthlyConfig, { height: 300 });
AdmindekChart.create('#sales-chart', weeklyConfig, { height: 300 });
// Look up an existing chart from its container
const canvas = document.querySelector('#sales-chart canvas');
const existing = Chart.getChart(canvas); // undefined if there is none
// Remove a chart completely (the canvas stays, empty)
if (existing) existing.destroy();Prefer chart.update() over recreating when only the data changes; it animates and is cheaper. Do not call new Chart() on a canvas that already has a chart: Chart.js throws "Canvas is already in use".
Performance Optimization #
Lazy Loading Charts #
Create charts only when their container scrolls into view:
const chartConfigs = {
'traffic-chart': () => AdmindekChart.create('#traffic-chart', trafficConfig, { height: 300 }),
'orders-chart': () => AdmindekChart.create('#orders-chart', ordersConfig, { height: 300 })
};
const chartObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const build = chartConfigs[entry.target.id];
if (build) build();
chartObserver.unobserve(entry.target);
});
});
Object.keys(chartConfigs).forEach((id) => {
const el = document.getElementById(id);
if (el) chartObserver.observe(el);
});Tips #
- Load
chart.umd.min.jsand the plugins only on pages that have charts. - Use
chart.update('none')for frequent updates. - For long series, set
animation: falseand keep points hidden (the preset already setsradius: 0).
Troubleshooting #
Chart not rendering:
console.log(typeof Chart); // "function" - chart.umd.min.js loaded
console.log(typeof AdmindekChart); // "object" - chart-theme.js loaded after it
console.log(document.querySelector('#chart-id')); // not nullAdmindekChart.create() returns null for a missing container.
Chart is 0px tall: pass { height: 300 } to create(), or give the container a CSS height if you set maintainAspectRatio: false yourself.
"Canvas is already in use": create charts with AdmindekChart.create(), which destroys the previous chart in that container, or update the existing instance instead of creating a new one.
Colours don't follow the preset colour: use theme tokens (gradient('primary')) or functions (() => AdmindekChart.color('primary')) instead of fixed values.
See Common Issues for more.
Summary #
Chart integration in Admindek provides:
- Chart.js 4 (MIT) with MIT plugins for heatmaps, candlesticks and zoom
- One theme preset (
chart-theme.js) so every chart matches the template - Automatic repainting on dark/light, RTL and preset-colour changes
- Small helpers (
create,radial,sparkline,gradient) that cut chart code to the data and a few options - Built-in centre labels and value labels for doughnuts and bars
- Live updates through the standard Chart.js
update()API