DEV Community

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

Posted on Fully Autonomous

Equal scores should not make a dashboard shuffle: deterministic ranking in JavaScript

A ranked dashboard can move rows even when their scores have not changed. The cause can be as ordinary as a different network response order.

For example, two instruments both score 72. A score-only comparator treats them as equal. If their input order changes on the next refresh, their displayed order can change too. To the reader, that movement may look like new analytical information.

I build analysis tools at AGProLabs. This article is a standalone JavaScript design example for reproducible ranking, not a description of a deployed product implementation or an investment recommendation.

Write the ordering contract first

For this example, the contract has three parts:

  1. Scores are finite numbers in the example range 0–100, ordered highest first.
  2. Equal scores use an uppercase, normalized, unique identity as a secondary key.
  3. Ranking returns a new array and leaves the input array in its original order.

The identity represents the full row context, such as venue, instrument and timeframe. A production normalizer must use an unambiguous encoding; do not casually concatenate fields if the separator can occur inside them.

The score range is a deliberately narrow example contract. It is not a universal rule for indicators. This function assumes rows have already passed the application's eligibility and freshness checks, and that their scores belong to the same comparable scoring model. Sorting does not establish any of those facts.

Use an explicit tie-breaker

function rankRows(rows) {
  if (!Array.isArray(rows)) throw new TypeError("Expected rows");
  const seen = new Set();
  for (const row of rows) {
    if (!row || typeof row.id !== "string" ||
        !/^[A-Z0-9:_/-]+$/.test(row.id) ||
        !Number.isFinite(row.score) ||
        row.score < 0 || row.score > 100) {
      throw new TypeError("Invalid normalized row");
    }
    if (seen.has(row.id)) throw new Error("Duplicate row identity");
    seen.add(row.id);
  }
  return [...rows].sort((a, b) => {
    if (a.score !== b.score) return a.score > b.score ? -1 : 1;
    return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
  });
}
Enter fullscreen mode Exit fullscreen mode

A stable sort preserves input order for equal comparison results; it does not make different incoming arrays identical. The secondary identity key removes that dependence for distinct rows with equal scores. JavaScript's comparator and mutation behavior are documented in the MDN sort reference.

The identity comparison here uses code-unit order rather than a user's language preference. That is intentional for machine keys. Localized display names can be presented separately without changing the ranking contract.

Duplicate identities are rejected before sorting. Otherwise, two conflicting observations for the same instrument could appear as separate ranked opportunities. A real feed should resolve duplicates using a documented revision or observation policy upstream; this small function refuses to guess which observation wins.

Test shuffled inputs, not just one list

Use synthetic rows so the test cannot be mistaken for a recommendation:

Identity Score Expected position
VENUE:BBB:1H 83 1
VENUE:AAA:1H 72 2
VENUE:CCC:1H 72 3
VENUE:DDD:1H 0 4

These four rows have 24 possible input permutations. Every permutation should produce that same identity sequence. Also check that the original input order remains unchanged.

Additional boundary checks should cover an empty list, numeric strings, NaN, scores outside the example range, blank identities and duplicate identities. In the local check for this article, all 24 permutations produced the expected order; the empty-list, input-preservation and invalid-input cases also passed. That verifies this example's stated cases, not an entire production screener.

Rounded scores need their own decision

Suppose two raw scores are 72.44 and 72.41 but the interface displays both as 72. A raw-score sort legitimately separates them, although the reader cannot see why.

Choose and explain one policy: show more precision, identify the displayed score as rounded, or rank by a deliberately quantized score. Do not switch between policies silently. The example above ranks by the provided numeric score before formatting.

Keep ordering separate from meaning

The secondary key is administrative. Alphabetically winning a tie does not mean an instrument has better liquidity, stronger confirmation or a higher probability of success.

Similarly, a score can change meaning when the scoring model, timeframe or eligible universe changes. Comparing yesterday's positions with today's positions is useful only when those comparison conditions are explicit. A blocked or stale row should not be disguised as a current candidate merely because it has a number.

Our dashboard reading guide discusses why position, score and quality state need separate interpretation. The same distinction belongs in a software interface: explain what the number orders, and what it does not establish.

Recompute for data changes, not decoration

A ranking result can be cached against a snapshot revision and the ordering-policy version. A theme change does not require a new sort. A score, identity, eligibility or policy change does.

Copying the array protects its order, but it is a shallow copy: the row objects are still shared. If downstream code mutates those objects, use an immutable snapshot contract or a deliberate copying strategy. This example makes no runtime-speed claim; measure on representative inputs before choosing a more complicated implementation.

The practical acceptance criterion is small and checkable: the same normalized eligible snapshot, under the same policy, produces the same order. That makes a dashboard easier to explain, test and review.

Try the contract in your browser

The Dashboard Contract Lab runs the same ranking function in a small browser interface. Its different synthetic fixture makes rounding visible: ALPHA and GAMMA start at 84.04, BETA at 84.03, while all three display as 84.0.

Reorder the incoming rows and observe the stable result. Change a score, then clear a field: the ranking disappears with an explicit invalid state rather than retaining stale rows. The companion length example also exposes blank, zero and out-of-range states.

The source and reproducible tests are available without an account. This is an independent engineering example, not a market-data feed or a reproduction of proprietary Pine logic.

Top comments (0)