Painting Charts

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.

#

Chart series

Every resolver takes an optional { el }: the id or element whose nearest .screen ancestor supplies the paint. Each returns a canonical Fill; see Paint API for the shape.

  • series(i, count, { el }): the fill for series i of count from the screen's chart-series ramp, which opens on the screen's ink. Series are spread across the legible front of the ramp; the span is the public --framework-chart-series-span var, so a theme narrows it from CSS.
  • ramp({ el }): every chart-series slot as an array of Fills. CSS publishes how many there are in --framework-chart-series-count, so a longer ramp reaches JavaScript without a framework release. Slots past the end of a shorter ramp come back empty.

Full-color screens draw their series from a color ramp: the ink, then seven hues in a fixed order, with steps picked for the light and dark ground separately. Grayscale and limited-palette screens keep the shade ladder, and a theme still overrides both with its own ramp.

// A single ramp step for series 1 of 4.
var s = TRMNLPaint.series(1, 4, { el: "my-chart" });

// Every ramp slot at once.
var fills = TRMNLPaint.ramp({ el: "my-chart" });
#

Highcharts adapters

Adapters shape a resolved Fill, BorderFill or TypeSpec for one specific renderer. They copy resolved values into the renderer's native form; no adapter adds contrast heuristics, thresholds or substitute design rules.

  • toHighcharts(fill): a solid Fill returns a flat color string; a tile Fill returns a { pattern: { image, width, height, backgroundColor } } object for the Highcharts pattern-fill module. The field color is composited into the pattern image beneath the ink paths, matching CSS layer order, so the tile is self-contained. An empty Fill returns null.

The axis and label adapters live with their resolvers: toHighchartsAxis() on Painting Borders , toHighchartsText() on Painting Typography .

TRMNLCharts (see the next section) is the Highcharts composition layer over TRMNLPaint: its grid(), axisLine() and textStyle() methods select the documented chart roles and delegate resolution and conversion here. Adapters for other libraries (a Chart.js CanvasPattern, a D3 <pattern>) are a natural sibling shape. Until one ships, resolve a Fill and translate it yourself; the Fill already carries everything a library needs.

// Highcharts: flat color on solid panels, a self-contained pattern tile
// on 1- and 2-bit screens.
var hc = TRMNLPaint.toHighcharts(TRMNLPaint.series(0, 4, { el: "my-chart" }));

// Any other library: resolve a Fill and translate it however you like.
var fill = TRMNLPaint.bg("gray-30", { el: "my-chart" });
var css = fill.image
  ? fill.image + " repeat"   // dither tile
  : fill.color;              // solid
#

The TRMNLCharts API

TRMNLCharts ships in the same plugins.js runtime as TRMNLPaint and composes Highcharts options out of it. It resolves no paint of its own: every value it returns comes from a TRMNLPaint resolver and adapter. Chart puts it to work in line, bar, and gauge charts.

Every method takes the same optional { el } as the resolvers: the chart container id or element whose nearest .screen supplies the paint. Omit it on a single-screen plugin.

#

Resolving chart paint

  • paint(token, { el }): one palette token as a Highcharts fill, flat color in solid modes and a pattern object in dither modes.
  • series(i, count, { el }): the Highcharts fill for series i of count from the screen's chart-series ramp.
  • applySwatches({ el }): paints every element under the screen carrying data-chart-series="i" from that same ramp, so legend markers and series stay in lockstep. An optional data-chart-series-count sets the series total; it defaults to the number of tagged elements. Call it every time the chart builds.
#

Building the options

  • options({ el }): the recommended Highcharts options for the TRMNL aesthetic, with a transparent background, no animation, no chrome, and framework-resolved axes and type.
  • merge(base, overrides): a deep merge of two plain objects, where arrays and scalars replace. Layer your chart's own config over options() with it.
  • grid({ el, dir }): the grid-line options (gridLineColor, gridLineWidth, gridLineDashStyle) from the muted themed hairline, border step 65. dir: 'h' (the default) is the horizontal yAxis grid, 'v' the vertical xAxis grid.
  • axisLine({ el }): the lineColor and tickColor options from the black border rail, which axis and ticks share.
  • textStyle(role, { el }): a Highcharts text style for one framework typography role. It carries the resolved font properties and an opaque ink color, and sets textOutline: 'none' to kill the default white halo on data labels. Pass { stroke: '<token>' } for an intentional outline.
#

Keeping the chart current

  • watch(el, buildFn): builds the chart now and again whenever the device, scale, mode, dark-mode or theme classes change on the screen or on a wrapper above it. buildFn creates and returns the chart instance; the previous one is destroyed before each rebuild. Returns a stop function.

Highcharts numbers do not read CSS, so resolve heights, spacing, and offsets with TRMNLPaint.px() inside the build function. See Paint API .

var el = "my-chart";

// watch() rebuilds on every device, mode, dark-mode and theme change.
TRMNLCharts.watch(el, function () {
  var chart = Highcharts.chart(el, TRMNLCharts.merge(TRMNLCharts.options({ el: el }), {
    chart: { type: "column", height: TRMNLPaint.px(180, { el: el }) },
    xAxis: { categories: ["Mon", "Tue", "Wed"] },
    series: [
      { name: "Sent", data: [3, 5, 8], color: TRMNLCharts.series(0, 2, { el: el }) },
      { name: "Read", data: [2, 4, 6], color: TRMNLCharts.series(1, 2, { el: el }) }
    ]
  }));
  // Legend markers marked data-chart-series="0" / "1" pick up the same ramp.
  TRMNLCharts.applySwatches({ el: el });
  return chart;
});
#

Live example

The bar rows below are painted with TRMNLPaint.series() and TRMNLPaint.apply(), wrapped in TRMNLPaint.watch(), with no charting library involved. Change the device mode or Style in the screen picker and the bars repaint from the live cascade: flat colors on solid panels, dither tiles on 1- and 2-bit screens.

$48.6k Paper Sales
6 Branches
TRMNL Logo Paint API TRMNLPaint.apply
<!-- an empty, ID'd container to paint into -->
<div id="paint-strip" class="flex flex--col gap--small w--full"></div>

<script type="text/javascript">
  // plugins.js bundles TRMNLPaint; wait for it before painting.
  function whenReady(cb) {
    if (window.TRMNLPaint) return cb();
    window.addEventListener("load", function () {
      if (window.TRMNLPaint) cb();
    }, { once: true });
  }

  whenReady(function () {
    var el = "paint-strip";
    var DATA = [["Scranton", 12.4], ["Stamford", 9.8], ["Nashua", 8.6],
                ["Utica", 7.4], ["Albany", 6.2], ["Buffalo", 4.2]];
    // watch() repaints whenever the screen device/scale/mode/dark/theme classes change,
    // re-reading paint from the live cascade each time.
    TRMNLPaint.watch(el, function () {
      var box = document.getElementById(el);
      if (!box) return;
      box.innerHTML = "";
      DATA.forEach(function (d, i) {
        var row = document.createElement("div");
        row.className = "flex flex--row flex--center-y gap--small";
        var cap = document.createElement("span");
        cap.className = "label label--small";
        cap.style.cssText = "width:88px;flex:none;";
        cap.textContent = d[0];
        var rail = document.createElement("div");
        rail.style.cssText = "flex:1;height:16px;";
        var bar = document.createElement("div");
        bar.style.cssText = "height:100%;border-radius:4px;width:" + (d[1] / DATA[0][1] * 100) + "%;";
        // series(i, n) resolves the fill for series i from the screen's chart
        // ramp; apply() composites field + tile like the CSS pipeline.
        TRMNLPaint.apply(bar, TRMNLPaint.series(i, DATA.length, { el: el }));
        rail.appendChild(bar);
        row.appendChild(cap);
        row.appendChild(rail);
        box.appendChild(row);
      });
    });
  });
</script>