Chart
With careful, minimal styling choices, TRMNL can display a variety of numerical or time centric content as charts and graphs.
Usage
Any CDN-enabled JavaScript library may be used to develop charting interfaces, however the examples below leverage Highcharts and Chartkick.
Charts paint with explicit colors, so they historically ignored bit depth, dark mode and themes
while the rest of the screen adapted. The plugin runtime bundles a small
TRMNLCharts helper that resolves adaptive
paint from the live screen at build time. The methods below are the ones these examples use;
Painting Charts
Painting Charts
Chart series colors from the framework ramp, with Highcharts adapters
The chart resolvers pick evenly spaced series colors from the framework chart ramp, resolved through the live cascade. Adapters convert the resulting Fills into Highcharts color options.
carries the full signature list and the resolvers behind it.
-
series(i, n, { el }): the fill for series i of n from the screen's chart-series ramp. -
applySwatches({ el }): paints legend markers taggeddata-chart-series="i"from the same ramp. -
textStyle(role, { el }): resolves a framework typography role for SVG text. -
options({ el })andmerge(): the recommended adaptive Highcharts defaults, merged under your overrides. -
watch(el, buildFn): rebuilds the chart when device, scale, mode, dark mode or theme changes. -
paint(token, { el }): a single token, a flat color on solid panels or a dither pattern on 1- and 2-bit screens. -
grid({ el, dir })andaxisLine({ el }): the grid-line and axis/tick optionsoptions()already applies, for an axis you build by hand.
{ el } is the chart container id or
element. Omit it on a single-screen plugin.
TRMNLCharts is the Highcharts adapter
built on TRMNLPaint, the framework's
public JS paint API. Grid lines and plotted text use the same border and typography systems as the
rest of the screen. For anything beyond Highcharts, use TRMNLPaint directly: see
Paint API
Paint API
TRMNLPaint: read the live CSS cascade from JavaScript to resolve framework colors and tile patterns
TRMNLPaint is the framework's public JavaScript paint API. It reads the live CSS cascade (bit depth, dark mode, theme, limited palette, and tiles all resolved) and hands back a canonical Fill, so token mappings are never duplicated in JavaScript. Charts are just one consumer; any plugin can resolve framework colors from JS for any purpose.
.
Highcharts numeric dimensions do not read CSS automatically. Resolve heights, spacing, line widths, and label offsets with TRMNLPaint.px() inside TRMNLCharts.watch(). The watcher rebuilds the chart after a scale change, and the paint API supplies the new numbers.
If you set the height: null within your highchart's settings, the chart will automatically expand to fill the available space.
Take care to disable animation effects, otherwise your chart may be only partially captured by TRMNL's screenshot rendering service.
These examples load Highcharts and Chartkick from trmnl.com, so they render empty without network. Highcharts is a commercial library that TRMNL licenses, and a custom stack brings its own charting library and license.
Line Chart
Line charts effectively display trends over time. This example shows a simple line chart with customized styling to match the TRMNL aesthetic.
<!-- import Highcharts + Chartkick libraries -->
<script src="https://trmnl.com/js/highcharts/12.3.0/highcharts.js"></script>
<script src="https://trmnl.com/js/chartkick/5.0.1/chartkick.min.js"></script>
<!-- markup with empty, ID'd element for chart injection -->
<div class="view view--full">
<div class="layout layout--col gap--space-between">
<div class="grid grid--cols-3">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">25,388</span>
<span class="label">Pageviews</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">4,771</span>
<span class="label">Visitors</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">2.23</span>
<span class="label">Mins on Page</span>
</div>
</div>
</div>
<div id="chart-123" class="w--full"></div>
</div>
<div class="title_bar">
<img class="image" src="/images/plugins/simple-analytics--render.svg" alt="Simple Analytics Logo">
<span class="title">Simple Analytics</span>
<span class="instance">trmnl.com</span>
</div>
</div>
<script type="text/javascript">
var data = [["2024-06-09", 975],["2024-06-10", 840],["2024-06-11", 1004],["2024-06-12", 1308],["2024-06-13", 753],["2024-06-14", 600],["2024-06-15", 710],
["2024-06-16", 489],["2024-06-17", 510],["2024-06-18", 590],["2024-06-19", 610],["2024-06-20", 671],["2024-06-21", 512],["2024-06-22", 550],
["2024-06-23", 421],["2024-06-24", 315],["2024-06-25", 604],["2024-06-26", 672],["2024-06-27", 601],["2024-06-28", 705],["2024-06-29", 800],
["2024-06-30", 912],["2024-07-01", 1503],["2024-07-02", 1273],["2024-07-03", 1250],["2024-07-04", 1198],["2024-07-05", 1005],["2024-07-06", 1300],
["2024-07-07", 1103],["2024-07-08", 1004],["2024-07-09", 600]];
// Wait for Chartkick and the framework TRMNLCharts helper (bundled in the
// plugin runtime), then read adaptive paint from the live screen.
function whenReady(cb) {
if (window.TRMNLCharts && window.Chartkick) return cb();
window.addEventListener("load", function () {
if (window.TRMNLCharts && window.Chartkick) cb();
}, { once: true });
}
whenReady(function () {
var el = "chart-123";
// watch() rebuilds on device/scale/mode/dark/theme change; series() resolves plotted-data
// paint from the framework chart ramp.
TRMNLCharts.watch(el, function () {
var px = function (value) { return TRMNLPaint.px(value, { el: el }); };
var linePaint = TRMNLCharts.series(0, 1, { el: el });
return new Chartkick.LineChart(el, data, {
adapter: "highcharts", // chartjs, google, etc available
prefix: "",
thousands: ",",
points: false,
colors: [linePaint],
curve: true,
// options() supplies the adaptive grid + label paint; layer the
// chart-specific overrides on top with merge().
library: TRMNLCharts.merge(TRMNLCharts.options({ el: el }), {
chart: { height: px(260) },
plotOptions: { series: { lineWidth: px(4) } },
yAxis: {
gridLineDashStyle: "shortdot",
tickAmount: 5
},
xAxis: {
type: "daytime",
lineWidth: 0,
gridLineDashStyle: "dot",
tickWidth: 1,
tickLength: 0,
tickPixelInterval: px(120)
}
})
});
});
});
</script>
Multi-Series Line Chart
For comparing data across multiple time periods or categories, multi-series line charts are ideal. This example demonstrates a comparison between current and previous period data with distinct styling for each series.
<!-- import required libraries -->
<script src="https://trmnl.com/js/highcharts/12.3.0/highcharts.js"></script>
<script src="https://trmnl.com/js/highcharts/12.3.0/highcharts-more.js"></script>
<script src="https://trmnl.com/js/highcharts/12.3.0/pattern-fill.js"></script>
<div class="view view--full">
<div class="layout layout--col gap--space-between">
<!-- Optional data metrics displayed above chart -->
<div class="grid">
<div class="row">
<div class="grid">
<div class="item col--span-2">
<div class="meta"></div>
<div class="content">
<span class="value value--large value--tnums">$85,240</span>
<span class="label">Total Sales</span>
</div>
</div>
<div class="item col--span-1">
<div class="meta"></div>
<div class="content">
<span class="value value--small value--tnums">32</span>
<span class="label">Pending Orders</span>
</div>
</div>
<div class="item col--span-1">
<div class="meta"></div>
<div class="content">
<span class="value value--xsmall value--tnums">
<div class="w--14 h--1.5 mb--2 rounded--full" data-chart-series="0" data-chart-series-count="2"></div>
Jul 01 - Jul 15
</span>
<span class="label">Current</span>
</div>
</div>
</div>
</div>
</div>
<div class="border--h-5 w--full"></div>
<!-- More metrics if needed -->
<div class="grid">
<div class="row">
<div class="grid">
<div class="item col--span-2">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">$128</span>
<span class="label">AOV</span>
</div>
</div>
<div class="item col--span-1">
<div class="meta"></div>
<div class="content">
<span class="value value--small value--tnums">665</span>
<span class="label">Fulfilled Orders</span>
</div>
</div>
<div class="item col--span-1">
<div class="meta"></div>
<div class="content">
<span class="value value--xsmall value--tnums">
<div class="w--14 h--1.5 mb--2" data-chart-series="1" data-chart-series-count="2"></div>
Jun 15 - Jun 30
</span>
<span class="label">Previous</span>
</div>
</div>
</div>
</div>
</div>
<!-- Chart container with unique ID -->
<div id="multi-series-chart" class="w--full"></div>
<script type="text/javascript">
// Using same date range for both series to ensure proper overlap
var currentPeriod = [
["2024-07-01", 3500], ["2024-07-02", 4200], ["2024-07-03", 3800],
["2024-07-04", 5100], ["2024-07-05", 4800], ["2024-07-06", 3600],
["2024-07-07", 2900], ["2024-07-08", 4300], ["2024-07-09", 5200],
["2024-07-10", 6100], ["2024-07-11", 5700], ["2024-07-12", 4900],
["2024-07-13", 5300], ["2024-07-14", 5800], ["2024-07-15", 6500]
];
// Using same date range but different values for comparison
var previousPeriod = [
["2024-07-01", 2800], ["2024-07-02", 3100], ["2024-07-03", 3400],
["2024-07-04", 3900], ["2024-07-05", 4500], ["2024-07-06", 4100],
["2024-07-07", 3700], ["2024-07-08", 3300], ["2024-07-09", 4200],
["2024-07-10", 4800], ["2024-07-11", 5100], ["2024-07-12", 4700],
["2024-07-13", 5400], ["2024-07-14", 5800], ["2024-07-15", 5600]
];
var formattedData = [
{ name: "Current", data: currentPeriod },
{ name: "Previous", data: previousPeriod }
];
// Wait for Highcharts and the framework TRMNLCharts helper (bundled in the
// plugin runtime) before building the chart.
function whenReady(cb) {
if (window.TRMNLCharts && window.Highcharts) return cb();
window.addEventListener("load", function () {
if (window.TRMNLCharts && window.Highcharts) cb();
}, { once: true });
}
whenReady(function () {
var el = "multi-series-chart";
// watch() rebuilds when the screen device/scale/mode/dark/theme changes, re-reading
// adaptive paint from the live cascade each time.
TRMNLCharts.watch(el, function () {
var px = function (value) { return TRMNLPaint.px(value, { el: el }); };
// series(i, 2) draws each line from the framework chart-series ramp
// (ink for the first, a legible step toward the canvas for the second),
// and applySwatches() paints the matching legend markers.
var chart = Highcharts.chart(el, TRMNLCharts.merge(TRMNLCharts.options({ el: el }), {
chart: { type: "spline", height: px(203), spacing: px([10, 10, 5, 10]) },
series: [{
data: formattedData[0].data,
name: formattedData[0].name,
lineWidth: px(4),
color: TRMNLCharts.series(0, 2, { el: el }),
zIndex: 2
}, {
data: formattedData[1].data,
name: formattedData[1].name,
lineWidth: px(5),
color: TRMNLCharts.series(1, 2, { el: el }),
zIndex: 1
}],
yAxis: {
gridLineDashStyle: "shortdot",
tickAmount: 5
},
xAxis: {
type: "datetime",
labels: { padding: px(5), y: px(25) },
lineWidth: 0,
gridLineDashStyle: "dot",
tickWidth: 1,
tickLength: 0,
tickPixelInterval: px(120)
}
}));
TRMNLCharts.applySwatches({ el: el });
return chart;
});
});
</script>
</div>
<div class="title_bar">
<img class="image image--adaptive" src="/images/plugins/trmnl--render.svg">
<span class="title">Charts</span>
<span class="instance">Multi-Series Line Chart</span>
</div>
</div>
Bar Chart
Bar charts are ideal for comparing discrete categories side by side. This example displays four different metrics across multiple time periods.
<!-- import Highcharts library -->
<script src="https://trmnl.com/js/highcharts/12.3.0/highcharts.js"></script>
<script src="https://trmnl.com/js/highcharts/12.3.0/pattern-fill.js"></script>
<div class="view view--full">
<div class="layout layout--col gap--space-between">
<!-- Business metrics displayed above chart -->
<div class="grid grid--cols-4">
<div class="item">
<div class="meta"></div>
<div class="content">
<div class="w--14 h--1.5 mb--2 rounded--full" data-chart-series="0" data-chart-series-count="4"></div>
<span class="value value--tnums">$31,883</span>
<span class="label">Revenue</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<div class="w--14 h--1.5 mb--2" data-chart-series="1" data-chart-series-count="4"></div>
<span class="value value--tnums">$22,910</span>
<span class="label">Expenses</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<div class="w--14 h--1.5 mb--2" data-chart-series="2" data-chart-series-count="4"></div>
<span class="value value--tnums">$8,990</span>
<span class="label">Marketing</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<div class="w--14 h--1.5 mb--2" data-chart-series="3" data-chart-series-count="4"></div>
<span class="value value--tnums">$14,930</span>
<span class="label">Operations</span>
</div>
</div>
</div>
<div class="border--h-5 w--full"></div>
<!-- Chart container with unique ID -->
<div id="example-bar-chart" class="w--full"></div>
<script type="text/javascript">
// Simplified regional data across four quarters
var revenueData = [
["Jan", 5883],
["Feb", 5260],
["Mar", 4760],
["Apr", 5120],
["May", 5540],
["Jun", 6320]
];
var expensesData = [
["Jan", 3580],
["Feb", 3210],
["Mar", 3620],
["Apr", 3950],
["May", 4120],
["Jun", 4430]
];
var marketingData = [
["Jan", 1120],
["Feb", 980],
["Mar", 1320],
["Apr", 1650],
["May", 1820],
["Jun", 2100]
];
var operationsData = [
["Jan", 2240],
["Feb", 2170],
["Mar", 2380],
["Apr", 2520],
["May", 2730],
["Jun", 2890]
];
var formattedBarData = [
{ name: "Revenue", data: revenueData },
{ name: "Expenses", data: expensesData },
{ name: "Marketing", data: marketingData },
{ name: "Operations", data: operationsData }
];
// Wait for Highcharts and the framework TRMNLCharts helper (bundled in the
// plugin runtime) before building the chart.
function whenReady(cb) {
if (window.TRMNLCharts && window.Highcharts) return cb();
window.addEventListener("load", function () {
if (window.TRMNLCharts && window.Highcharts) cb();
}, { once: true });
}
whenReady(function () {
var el = "example-bar-chart";
// series(i, 4) draws each bar from the framework chart-series ramp, and
// applySwatches() paints the matching legend markers from the same ramp,
// so bars and swatches stay in lockstep in every bit depth, dark mode and theme.
TRMNLCharts.watch(el, function () {
var px = function (value) { return TRMNLPaint.px(value, { el: el }); };
var chart = Highcharts.chart(el, TRMNLCharts.merge(TRMNLCharts.options({ el: el }), {
chart: { type: "column", height: px(284), spacing: px([10, 10, 5, 10]) },
plotOptions: { series: { pointPadding: 0.05, groupPadding: 0.1, borderWidth: 0 } },
series: [{
data: formattedBarData[0].data,
name: formattedBarData[0].name,
color: TRMNLCharts.series(0, 4, { el: el }),
zIndex: 4
}, {
data: formattedBarData[1].data,
name: formattedBarData[1].name,
color: TRMNLCharts.series(1, 4, { el: el }),
zIndex: 3
}, {
data: formattedBarData[2].data,
name: formattedBarData[2].name,
color: TRMNLCharts.series(2, 4, { el: el }),
zIndex: 2
}, {
data: formattedBarData[3].data,
name: formattedBarData[3].name,
color: TRMNLCharts.series(3, 4, { el: el }),
zIndex: 1
}],
yAxis: {
gridLineDashStyle: "shortdot",
tickAmount: 5
},
xAxis: {
type: "category",
labels: { padding: px(5), y: px(25) },
lineWidth: 0,
gridLineDashStyle: "dot",
tickWidth: 0,
tickLength: 0
}
}));
TRMNLCharts.applySwatches({ el: el });
return chart;
});
});
</script>
</div>
</div>
Gauge Chart
Gauge charts can effectively display single metrics or scores. This example shows multiple gauges in a row with a main summary gauge, perfect for displaying daily and weekly metrics like sleep quality scores.
<!-- import Highcharts libraries -->
<script src="https://trmnl.com/js/highcharts/12.3.0/highcharts.js"></script>
<script src="https://trmnl.com/js/highcharts/12.3.0/highcharts-more.js"></script>
<script src="https://trmnl.com/js/highcharts/12.3.0/pattern-fill.js"></script>
<div class="view view--full">
<div class="layout layout--col gap--none">
<div class="grid grid--cols-7 mb--5">
<div class="h--32">
<div id="day_0" class="h--24"></div>
<span class="description text--center">Monday</span>
</div>
<div class="h--32">
<div id="day_1" class="h--24"></div>
<span class="description text--center">Tuesday</span>
</div>
<div class="h--32">
<div id="day_2" class="h--24"></div>
<span class="description text--center">Wednesday</span>
</div>
<div class="h--32">
<div id="day_3" class="h--24"></div>
<span class="description text--center">Thursday</span>
</div>
<div class="h--32">
<div id="day_4" class="h--24"></div>
<span class="description text--center">Friday</span>
</div>
<div class="h--32">
<div id="day_5" class="h--24"></div>
<span class="description text--center">Saturday</span>
</div>
<div class="h--32">
<div id="day_6" class="h--24"></div>
<span class="description text--center">Sunday</span>
</div>
</div>
<div class="divider"></div>
<div class="grid">
<div class="col--span-1 col--center">
<div id="day_all"></div>
</div>
<div class="col--span-1 gap--large">
<div class="flex flex--col gap--medium w--full flex--center">
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">18%</span>
<span class="label">REM Sleep</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--tnums">23%</span>
<span class="label">Deep Sleep</span>
</div>
</div>
</div>
<div class="divider"></div>
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--small value--tnums">12m</span>
<span class="label">Time to Sleep</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--small value--tnums">7h 32min</span>
<span class="label">Sleep Duration</span>
</div>
</div>
</div>
<div class="divider"></div>
<div class="grid grid--cols-2">
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--small value--tnums">8</span>
<span class="label">Toss & Turns</span>
</div>
</div>
<div class="item">
<div class="meta"></div>
<div class="content">
<span class="value value--small value--tnums">0.5%</span>
<span class="label">Snoring</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var dailyScores = [92, 95, 81, 56, 81, 72, 85];
var weeklyScore = 82;
function createGauge(score, day, opts) {
opts ||= {
big: true,
height: "80%",
labels: { distance: 15 },
rating: textRating(score)
};
var el = "day_" + day;
var px = function (value) { return TRMNLPaint.px(value, { el: el }); };
var labels = { ...(opts.labels || {}) };
if (typeof labels.distance === "number") labels.distance = px(labels.distance);
// options() supplies the adaptive text + axis paint; the gauge-specific config
// is layered on top with merge(). textStyle() plots the value in the framework
// .value role (weekly) or text--small (daily gauges).
return Highcharts.chart(el, TRMNLCharts.merge(TRMNLCharts.options({ el: el }), {
chart: {
type: "gauge",
height: opts.height
},
pane: {
startAngle: -150,
endAngle: 150,
background: {
backgroundColor: "transparent",
borderWidth: 0
}
},
plotOptions: {
gauge: {
animation: false,
pivot: {
backgroundColor: "transparent"
},
dial: {
backgroundColor: "transparent",
baseWidth: 0
}
}
},
yAxis: {
min: 0,
max: 100,
minorTickInterval: 0,
tickLength: px(40),
tickPixelInterval: px(40),
tickWidth: 0,
lineWidth: 0,
gridLineWidth: 0,
title: {
text: opts.rating,
style: TRMNLCharts.textStyle("chart-label", { el: el })
},
labels: {
...labels,
style: TRMNLCharts.textStyle("chart-label", { el: el })
},
plotBands: [{
from: 1,
to: score,
color: TRMNLCharts.series(0, 2, { el: el }),
innerRadius: "82%",
borderRadius: "50%"
}, {
from: score + 1,
to: 100,
color: TRMNLCharts.series(1, 2, { el: el }),
innerRadius: "82%",
borderRadius: "50%"
}]
},
series: [{
name: "Score",
data: [score],
dataLabels: {
borderWidth: 0,
style: TRMNLCharts.textStyle(opts.big ? "value" : "chart-label", { el: el })
}
}]
}));
}
function textRating(score) {
if (score <= 50) {
return "Low";
} else if (score <= 65) {
return "Pay Attention";
} else if (score < 80) {
return "Fair";
} else {
return "Good";
}
}
// Wait for Highcharts + the framework TRMNLCharts helper, then build all gauges
// and rebuild them whenever the screen device/scale/mode/dark/theme changes.
function whenReady(cb) {
if (window.TRMNLCharts && window.Highcharts) return cb();
window.addEventListener("load", function () {
if (window.TRMNLCharts && window.Highcharts) cb();
}, { once: true });
}
whenReady(function () {
// watch() tracks one instance; return a composite whose destroy() tears down
// every gauge before the next rebuild.
TRMNLCharts.watch("day_all", function () {
var charts = [];
// Small daily gauges (value in the text--small role, no rating label)
dailyScores.forEach(function (score, idx) {
charts.push(createGauge(score, idx, {
big: false,
labels: { enabled: false },
rating: null
}));
});
// Main weekly gauge: big=true plots the score in the same .value
// role as the stat tiles beside it.
charts.push(createGauge(weeklyScore, "all", {
big: true,
height: "80%",
labels: { distance: 15 },
rating: textRating(weeklyScore)
}));
return { destroy: function () {
charts.forEach(function (c) { try { c.destroy(); } catch (e) {} });
} };
});
});
</script>