DEV Community

Ali Gürtuna | AGProLabs
Ali Gürtuna | AGProLabs

Posted on Fully Autonomous

Do not render unavailable as zero: a unit-aware JavaScript metric formatter

A blank card, an observed zero and a malformed number are three different states. Formatting all three as 0.00% may produce a tidy interface, but it removes information the reader needs.

I maintain AGProLabs, where chart-reading interfaces make this distinction useful. This post offers a small independent JavaScript display contract. It is not proprietary indicator source, a market-data feed, a clinical assessment or a financial calculation. All test values below are synthetic.

Start with a declared unit

Consider two inputs:

  • { value: 0.25, unit: "share" } means a share between zero and one; the display should be 25.00%.
  • { value: 25, unit: "percentage" } already uses percentage units; multiplying it again would be a display error.

The number alone cannot resolve that distinction. Give the producer a contract and reject unsupported units at the boundary. Here, share is deliberately restricted to [0, 1]. It is not a generic ratio or a percentage change. percentage permits negative values and values above 100 because this formatter does not know the domain meaning. A nonnegative count must be a safe integer.

Preserve the missing state

The producer sends either an observed record with a value and unit, or an unavailable record without a value. A missing record carrying value: 0 is contradictory and rejected. A numeric string is rejected instead of silently parsed. That makes schema drift visible to the calling component.

This is a rendering contract, not evidence validation. The word observed in a payload does not prove that an upstream source actually measured anything. Source identity, observation time, interval and authorization belong to separate checks.

The module

Save this as metric-display.mjs:

export function formatMetric(record, decimals = 2) {
  const bad = reason => ({ status: "invalid", text: "Invalid", reason });
  if (!Number.isInteger(decimals) || decimals < 0 || decimals > 6)
    return bad("invalid precision");
  if (!record || typeof record !== "object" || Array.isArray(record))
    return bad("not a metric record");
  if (record.state === "unavailable") {
    return Object.hasOwn(record, "value")
      ? bad("unavailable record contains a value")
      : { status: "unavailable", text: "Unavailable" };
  }
  if (record.state !== "observed") return bad("unknown state");
  const { value, unit } = record;
  if (typeof value !== "number" || !Number.isFinite(value))
    return bad("not a finite number");
  if (!["percentage", "share", "count"].includes(unit))
    return bad("unknown unit");
  if (unit === "share" && (value < 0 || value > 1))
    return bad("share outside zero to one");
  if (unit === "count" && (!Number.isSafeInteger(value) || value < 0))
    return bad("not a nonnegative safe count");
  const displayed = unit === "share" ? value * 100 : value;
  const places = unit === "count" ? 0 : decimals;
  let text = displayed.toFixed(places);
  // A rounded negative zero must not appear as a negative change.
  if (Number(text) === 0) text = (0).toFixed(places);
  return { status: "ready", text: text + (unit === "count" ? "" : "%") };
}
Enter fullscreen mode Exit fullscreen mode

The component receives a status as well as text. It can render unavailable and invalid values differently, while retaining the diagnostic reason in an appropriate developer-facing channel. Do not expose raw private payloads in a public error message.

The precision parameter is bounded to zero through six decimal places. Counts ignore fractional precision. A negative value rounded to zero is displayed as zero at the requested precision; that does not mean the original value was exactly zero. The example uses toFixed for a small deterministic contract, not a complete locale-aware financial display. Very large values may use exponential notation. If your interface needs currency, scientific units, localized grouping or exact decimal arithmetic, define and test those requirements separately.

Run the cases before connecting a UI

Save this next to the module as metric-display.test.mjs, then run node metric-display.test.mjs. The module uses Object.hasOwn and the test uses Node's built-in assertion module; use a runtime that supports those APIs.

import assert from "node:assert/strict";
import { formatMetric } from "./metric-display.mjs";
const observed = (value, unit) => ({ state: "observed", value, unit });
const cases = [
  ["observed zero stays zero", observed(0, "percentage"), "ready", "0.00%"],
  ["unavailable is not zero", { state: "unavailable" }, "unavailable", "Unavailable"],
  ["contradictory missing value", { state: "unavailable", value: 0 }, "invalid", "Invalid"],
  ["numeric string rejected", observed("25", "percentage"), "invalid", "Invalid"],
  ["infinity rejected", observed(Infinity, "percentage"), "invalid", "Invalid"],
  ["NaN rejected", observed(NaN, "percentage"), "invalid", "Invalid"],
  ["share conversion", observed(0.25, "share"), "ready", "25.00%"],
  ["percentage not converted again", observed(25, "percentage"), "ready", "25.00%"],
  ["share outside contract", observed(25, "share"), "invalid", "Invalid"],
  ["negative percentage preserved", observed(-12.5, "percentage"), "ready", "-12.50%"],
  ["integer count", observed(12, "count"), "ready", "12"],
  ["fractional count rejected", observed(1.5, "count"), "invalid", "Invalid"],
  ["negative count rejected", observed(-1, "count"), "invalid", "Invalid"],
  ["unsafe integer rejected", observed(Number.MAX_SAFE_INTEGER + 1, "count"), "invalid", "Invalid"],
  ["unknown unit rejected", observed(1, "price"), "invalid", "Invalid"],
  ["unknown state rejected", { state: "estimated", value: 1, unit: "count" }, "invalid", "Invalid"],
  ["rounded negative zero normalized", observed(-0.0001, "percentage"), "ready", "0.00%"],
  ["null record rejected", null, "invalid", "Invalid"]
];
for (const [name, record, status, text] of cases) {
  const result = formatMetric(record);
  assert.equal(result.status, status, name);
  assert.equal(result.text, text, name);
}
assert.equal(formatMetric(observed(1, "percentage"), 7).status, "invalid");
assert.equal(formatMetric(observed(1, "percentage"), -1).status, "invalid");
console.log(JSON.stringify({ passed: cases.length + 2, failed: 0 }));
Enter fullscreen mode Exit fullscreen mode

Local result for this exact example: 20 passed, 0 failed. The first eighteen named cases cover the payloads and displayed text; two additional assertions cover invalid precision. That result applies to these synthetic cases. It is not a cross-browser test, an accessibility review, a performance benchmark or a guarantee about your live producer.

Decide what the component should retain

For a ready record, display the formatted value with its field label. For an unavailable record, preserve the missing state and offer an appropriate explanation. For an invalid record, avoid rendering a plausible substitute number. A UI test should confirm that the same states remain distinct after CSS and theme changes.

Retain the original unit in your observation record. In a journal, a label such as win fraction, percentage change or resolved-event count should not disappear when you copy a screenshot. A count is also not a denominator by itself: you need a separately identified sample to interpret a percentage.

Owner-maintained examples of the reading problem

These are resources from my own projects, provided as related context rather than independent endorsements:

The practical rule is to keep the value, unit and availability separate until the display boundary. The caller still needs to establish what the value represents. A clean-looking zero should never be used to hide a missing or invalid record.

AI disclosure: prepared with AI assistance for the author's own projects. The example was executed locally with the tests shown. No private account, medical or live market data is used or collected. Educational software example only; no trading prediction, financial advice or guaranteed outcome.

Try the fixed examples in a browser

Open the independent Metric Display Lab. It offers eight synthetic records using the formatter above. Select a record, render it, and reset the page. Changing the selection clears the previous result.

The live Chrome check covered all eight fixtures, clearing between selections, and reset. The separate module still has its 20 local assertions shown above. These focused checks are not a full accessibility or cross-browser certification. No live market feed, analytics, local storage or personal input is involved. The lab is an independently designed educational example for the author’s own projects.

Top comments (0)