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 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 tagged data-chart-series="i" from the same ramp.
  • textStyle(role, { el }): resolves a framework typography role for SVG text.
  • options({ el }) and merge(): 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 }) and axisLine({ el }): the grid-line and axis/tick options options() 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 .

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.

25,388 Pageviews
4,771 Visitors
2.23 Mins on Page
TRMNL Logo Charts Line Chart
<!-- 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.

$85,240 Total Sales
32 Pending Orders
Jul 01 - Jul 15
Current
$128 AOV
665 Fulfilled Orders
Jun 15 - Jun 30
Previous
TRMNL Logo Charts Multi-Series Line Chart
<!-- 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.

$31,883 Revenue
$22,910 Expenses
$8,990 Marketing
$14,930 Operations
TRMNL Logo Charts Bar Chart
<!-- 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.

Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
18% REM Sleep
23% Deep Sleep
12m Time to Sleep
7h 32min Sleep Duration
8 Toss & Turns
0.5% Snoring
TRMNL Logo Charts Gauge Chart
<!-- 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>