BarChart value labels misstate non-integer data

Author: oujinhaoaiCreated Sep 13, 2026Updated Sep 13, 2026

Summary

BarChart value labels misstate the data they annotate. formatNumber returns n.toFixed(1) for any non-integer, so a chart of USD costs [0, 0.15, 0.69, 1.33] renders labels 0, 0.1, 0.7, 1.3 while the bars are drawn at the true heights — the graphic and its own annotation disagree, and the chart states numbers that are not the input.

This is a data-correctness bug rather than a styling one, and it is invisible to the pipeline's own quality gate: final_review's visual_spotcheck checks for black frames, broken overlays, missing assets and unreadable text, but never compares rendered chart values against the source data. A chart stating wrong numbers passes self-review with status: pass.

Found while producing a zero-key animated-explainer video whose whole premise was a cost comparison — the chart was quietly wrong in three of four bars.

Environment: commit 08e2151, Python 3.10, Node v26.7.0, playbook flat-motion-graphics.

Operating system

macOS 15 (Darwin 25.6.0), Mac mini M4

Pipeline

animated-explainer

Runtime / renderer

remotion

Steps to reproduce

  1. Render an Explainer composition containing a bar_chart cut with showValues: true:

    json
    {
      "id": "costs",
      "type": "bar_chart",
      "in_seconds": 0,
      "out_seconds": 6,
      "title": "What a finished video costs (USD)",
      "showValues": true,
      "chartData": [
        { "label": "This video",  "value": 0 },
        { "label": "Ghibli anim", "value": 0.15 },
        { "label": "Product ad",  "value": 0.69 },
        { "label": "Pixar short", "value": 1.33 }
      ]
    }
  2. Extract a frame inside that cut and read the value labels above the bars.

Expected behavior

Value labels match the data passed in: 0, 0.15, 0.69, 1.33.

Actual behavior

Labels render 0, 0.1, 0.7, 1.3. Bar heights still use the true values, so the tallest bar is annotated 1.3 while being drawn at 1.33, and 0.15 is annotated as 0.1 — a 33% understatement.

remotion-composer/src/components/charts/BarChart.tsx:279-284:

javascript
function formatNumber(n: number): string {
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
  if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
  if (Number.isInteger(n)) return String(n);
  return n.toFixed(1);
}

Relevant logs or error output

No error is produced. The render exits 0 and final_review returns:

  "visual_spotcheck": {
    "black_frames_detected": false,
    "broken_overlays": false,
    "missing_assets": false,
    "unreadable_text": false,
    "issues": []
  },
  "status": "pass"

The only way I found this was extracting a frame and reading the chart.

Notes

Suggested fix: derive significant digits from the data's magnitude, or expose a valueFormat / decimals prop so callers can state precision. #518 adds exact formatting controls to KPIGrid; BarChart has the same need and is untouched by it.

Workaround in the meantime: express values in integer units (cents rather than dollars) — integers take the String(n) path and render exactly.

Two adjacent items found in the same run, both of which I could not find reported in any open or closed issue/PR — happy to split them out or fold them into a PR:

  • ProgressBar's trackColor default #E5E7EB is never forwarded by Explainer, leaving a bright light-grey track on dark playbooks.
  • progressSegments[].value is a width percentage (segWidth = (seg.value / 100) * 100), so seven equal segments need value: 14.29 each rather than value: 1. Nothing documents this and the schema does not constrain it; passing 1 silently fills 7% of the bar.

I can open a PR with the fix plus a contract test if that is welcome.