mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
fix(reference): resolve the normal band by marker, not index 0
The reference-range classifier assumed bands[0] was always the normal/target band, so any metric whose first band is abnormal was placed wrong: a healthy BMI 22 read as just outside the reference and an underweight BMI 17 read as inside; pulse pressure had the same flaw (Narrow is its first band). The same classifier drives the in-app four-state explainer, so both surfaces were affected. Mark the population-normal band explicitly with a `normal: true` flag on every metric, add `normalBandIndex`, and place values by distance from the marked band rather than from index 0. `headlineBandText` now renders the marked band, so BMI grounds against 18.5-24.9 kg/m2. Exclude daily steps from the Coach grounding: it is fed the intra-day mean of the step rows, which averages a multi-row day down and can misplace it below the 8,000 floor. Grounding it on a per-day sum is the correct fix and is left as a follow-up; until then drop the line rather than ship a wrong placement. Extend the classifier and grounding tests for BMI 17/22/27, a pulse pressure case, the normal-band-not-at-index-0 contract, and that steps is not grounded.
This commit is contained in:
1 parent
22ffb19fa5
commit
9265ab89c6
4 files changed
+175
-33
No files matched your search
@@ -7,6 +7,7 @@ import {
|
||||
classifyReference,
|
||||
getReferenceRange,
|
||||
isReferenceMetric,
|
||||
normalBandIndex,
|
||||
referenceCitation,
|
||||
referenceLabel,
|
||||
type ReferenceMetric,
|
||||
@@ -54,6 +55,42 @@ describe("reference-ranges backbone", () => {
|
||||
expect(classifyReference("OXYGEN_SATURATION", 98)).toBe("within");
|
||||
});
|
||||
|
||||
it("every banded metric marks exactly one normal band", () => {
|
||||
for (const metric of REFERENCE_METRICS) {
|
||||
const bands = REFERENCE_RANGES[metric].bands;
|
||||
if (bands.length === 0) continue;
|
||||
const marked = bands.filter((b) => b.normal === true);
|
||||
expect(marked, `${metric} normal-band count`).toHaveLength(1);
|
||||
// `normalBandIndex` points at that marked band.
|
||||
expect(bands[normalBandIndex(metric)].normal, `${metric} index`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves the normal band by marker, not by index 0", () => {
|
||||
// BMI authors Underweight FIRST; the normal band is the 18.5–24.9 entry.
|
||||
expect(REFERENCE_RANGES.BMI.bands[0].label).toBe("Underweight");
|
||||
expect(REFERENCE_RANGES.BMI.bands[normalBandIndex("BMI")].label).toBe("Normal");
|
||||
// BMI 22 is healthy → within (the index-0 bug reported it slightly-outside).
|
||||
expect(classifyReference("BMI", 22)).toBe("within");
|
||||
// BMI 17 is underweight → one band below normal → slightly-outside
|
||||
// (the index-0 bug reported it as within).
|
||||
expect(classifyReference("BMI", 17)).toBe("slightly-outside");
|
||||
// BMI 27 overweight (one above) → slightly-outside; 35 obesity (two
|
||||
// above) → outside.
|
||||
expect(classifyReference("BMI", 27)).toBe("slightly-outside");
|
||||
expect(classifyReference("BMI", 35)).toBe("outside");
|
||||
});
|
||||
|
||||
it("classifies pulse pressure around its interior normal band", () => {
|
||||
// Narrow is the FIRST band; the normal band is "Typical at rest".
|
||||
expect(REFERENCE_RANGES.PULSE_PRESSURE.bands[0].label).toBe("Narrow");
|
||||
expect(classifyReference("PULSE_PRESSURE", 40)).toBe("within");
|
||||
// Narrow (≤25) is one band below normal → slightly-outside.
|
||||
expect(classifyReference("PULSE_PRESSURE", 20)).toBe("slightly-outside");
|
||||
// Wide (≥60) is one band above normal → slightly-outside.
|
||||
expect(classifyReference("PULSE_PRESSURE", 75)).toBe("slightly-outside");
|
||||
});
|
||||
|
||||
it("returns insufficient for absent values and band-less metrics", () => {
|
||||
expect(classifyReference("RESTING_HEART_RATE", null)).toBe("insufficient");
|
||||
expect(classifyReference("RESTING_HEART_RATE", Number.NaN)).toBe("insufficient");
|
||||
|
||||
@@ -113,6 +113,71 @@ describe("buildReferenceGroundingBlock", () => {
|
||||
expect(outOfGoal).toContain("outside the typical diabetes management goal");
|
||||
});
|
||||
|
||||
it("grounds BMI against the WHO normal band, not the first (Underweight) band", () => {
|
||||
// The headline band rendered must be the 18.5–24.9 normal range — the
|
||||
// index-0 bug surfaced the Underweight "≤18.5" ceiling instead.
|
||||
const normal = buildReferenceGroundingBlock({
|
||||
metrics: [{ metric: "BMI", value: 22 }],
|
||||
hasDiabetes: false,
|
||||
})!;
|
||||
const bmiLine = normal.split("\n").find((l) => l.startsWith("- BMI:"))!;
|
||||
expect(bmiLine).toContain("18.5–24.9 kg/m²");
|
||||
expect(bmiLine).not.toContain("≤18.5");
|
||||
// A healthy 22 sits INSIDE the band (was wrongly "just outside").
|
||||
expect(bmiLine).toContain("sits inside the general reference band");
|
||||
|
||||
// Underweight 17 sits just outside (was wrongly "inside").
|
||||
const under = buildReferenceGroundingBlock({
|
||||
metrics: [{ metric: "BMI", value: 17 }],
|
||||
hasDiabetes: false,
|
||||
})!;
|
||||
expect(under.split("\n").find((l) => l.startsWith("- BMI:"))).toContain(
|
||||
"sits just outside the general reference band",
|
||||
);
|
||||
|
||||
// Overweight 27 sits just outside.
|
||||
const over = buildReferenceGroundingBlock({
|
||||
metrics: [{ metric: "BMI", value: 27 }],
|
||||
hasDiabetes: false,
|
||||
})!;
|
||||
expect(over.split("\n").find((l) => l.startsWith("- BMI:"))).toContain(
|
||||
"sits just outside the general reference band",
|
||||
);
|
||||
});
|
||||
|
||||
it("grounds pulse pressure against its interior normal band", () => {
|
||||
const block = buildReferenceGroundingBlock({
|
||||
metrics: [{ metric: "PULSE_PRESSURE", value: 40 }],
|
||||
hasDiabetes: false,
|
||||
})!;
|
||||
const line = block.split("\n").find((l) => l.startsWith("- pulse pressure:"))!;
|
||||
// The normal "Typical at rest" band (25–60), not the first "Narrow" band.
|
||||
expect(line).toContain("25–60 mmHg");
|
||||
expect(line).toContain("sits inside the general reference band");
|
||||
});
|
||||
|
||||
it("does not ground steps (mean-aggregation misplacement; follow-up)", () => {
|
||||
const block = buildReferenceGroundingBlock({
|
||||
metrics: [
|
||||
{ metric: "STEPS", value: 9000 },
|
||||
{ metric: "BMI", value: 22 },
|
||||
],
|
||||
hasDiabetes: false,
|
||||
})!;
|
||||
// STEPS is dropped; the BMI line still renders.
|
||||
expect(block).not.toContain("daily steps");
|
||||
expect(block.split("\n").some((l) => l.startsWith("- BMI:"))).toBe(true);
|
||||
});
|
||||
|
||||
it("returns null when steps is the only metric (it is not grounded)", () => {
|
||||
expect(
|
||||
buildReferenceGroundingBlock({
|
||||
metrics: [{ metric: "STEPS", value: 9000 }],
|
||||
hasDiabetes: false,
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("treats a band-less metric (HRV) as baseline-led, never out-of-band", () => {
|
||||
const block = buildReferenceGroundingBlock({
|
||||
metrics: [{ metric: "HEART_RATE_VARIABILITY", value: 45 }],
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
import {
|
||||
classifyReference,
|
||||
getReferenceRange,
|
||||
normalBandIndex,
|
||||
referenceLabel,
|
||||
type ReferenceMetric,
|
||||
type ReferencePlacement,
|
||||
@@ -134,13 +135,16 @@ function placementPhrase(placement: ReferencePlacement): string {
|
||||
|
||||
/**
|
||||
* Render the half-open band as a compact "low–high unit" descriptor for the
|
||||
* metric's headline (first/normal) band. Open-ended bounds render with a
|
||||
* leading/trailing ≤ / ≥.
|
||||
* metric's NORMAL band — resolved by the `normal` marker, NOT by index 0
|
||||
* (some metrics author an abnormal band first, e.g. BMI Underweight or
|
||||
* pulse-pressure Narrow). Open-ended bounds render with a leading/trailing
|
||||
* ≤ / ≥.
|
||||
*/
|
||||
function headlineBandText(metric: ReferenceMetric): string | null {
|
||||
const range = getReferenceRange(metric);
|
||||
if (!range || range.bands.length === 0) return null;
|
||||
const band = range.bands[0];
|
||||
const idx = normalBandIndex(metric);
|
||||
const band = range.bands[idx];
|
||||
const unit = range.unit;
|
||||
if (band.low != null && band.high != null) {
|
||||
return `${band.low}–${band.high} ${unit}`;
|
||||
@@ -217,6 +221,13 @@ export function buildReferenceGroundingBlock(
|
||||
for (const m of input.metrics) {
|
||||
if (seen.has(m.metric)) continue;
|
||||
if (!getReferenceRange(m.metric)) continue;
|
||||
// STEPS is excluded from grounding for now: the caller feeds the
|
||||
// intra-day MEAN of the ACTIVITY_STEPS rows, so a day logged across
|
||||
// several rows is averaged DOWN and can be misclassified below the
|
||||
// 8,000-step floor. Grounding it on a sum-per-day aggregate is the
|
||||
// correct fix and is tracked as a follow-up; until then we drop the
|
||||
// line rather than ship a wrong placement.
|
||||
if (m.metric === "STEPS") continue;
|
||||
seen.add(m.metric);
|
||||
ordered.push(m);
|
||||
if (ordered.length >= MAX_GROUNDING_METRICS) break;
|
||||
|
||||
+59
-30
@@ -39,6 +39,16 @@ export interface ReferenceBand {
|
||||
readonly high?: number;
|
||||
/** The body + year that publishes this band. */
|
||||
readonly citation: CitationId;
|
||||
/**
|
||||
* Marks THE population-normal / target band — the placement
|
||||
* `classifyReference` treats as "within" and `headlineBandText` renders
|
||||
* as the headline. Exactly one band per metric (with at least one band)
|
||||
* carries this. Bands are NOT assumed normal by index: a metric whose
|
||||
* first band is abnormal (BMI → Underweight first; pulse pressure →
|
||||
* Narrow first) marks the correct interior band instead. A test asserts
|
||||
* the one-marker-per-banded-metric invariant.
|
||||
*/
|
||||
readonly normal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +65,12 @@ export interface ReferenceConflict {
|
||||
export interface ReferenceRange {
|
||||
/** Canonical display unit (matches the metric tile + registry). */
|
||||
readonly unit: string;
|
||||
/** Ordered population bands; the first is the "good"/normal placement. */
|
||||
/**
|
||||
* Ordered population bands. The "good"/normal placement is the band
|
||||
* flagged `normal: true` — NOT necessarily the first entry. Some metrics
|
||||
* (BMI, pulse pressure) author an abnormal band first, so consumers must
|
||||
* resolve the normal band by the marker, never by index.
|
||||
*/
|
||||
readonly bands: readonly ReferenceBand[];
|
||||
/** The body the headline band is sourced to (drives the cited sentence). */
|
||||
readonly referenceId: CitationId;
|
||||
@@ -95,7 +110,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
unit: "mmHg",
|
||||
bands: [
|
||||
// ESH grade bands (office, mmHg, systolic anchors shown).
|
||||
{ label: "Optimal", high: 120, citation: "ESH_2023_HYPERTENSION" },
|
||||
{ label: "Optimal", high: 120, citation: "ESH_2023_HYPERTENSION", normal: true },
|
||||
{ label: "Normal", low: 120, high: 129, citation: "ESH_2023_HYPERTENSION" },
|
||||
{ label: "High-normal", low: 130, high: 139, citation: "ESH_2023_HYPERTENSION" },
|
||||
{ label: "Hypertension", low: 140, citation: "ESH_2023_HYPERTENSION" },
|
||||
@@ -120,7 +135,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
unit: "mmHg",
|
||||
bands: [
|
||||
{ label: "Narrow", high: 25, citation: "STATPEARLS_PULSE_PRESSURE" },
|
||||
{ label: "Typical at rest", low: 25, high: 60, citation: "STATPEARLS_PULSE_PRESSURE" },
|
||||
{ label: "Typical at rest", low: 25, high: 60, citation: "STATPEARLS_PULSE_PRESSURE", normal: true },
|
||||
{ label: "Wide", low: 60, citation: "STATPEARLS_PULSE_PRESSURE" },
|
||||
],
|
||||
referenceId: "STATPEARLS_PULSE_PRESSURE",
|
||||
@@ -132,7 +147,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
MEAN_ARTERIAL_PRESSURE: {
|
||||
unit: "mmHg",
|
||||
bands: [
|
||||
{ label: "Resting band", low: 70, high: 100, citation: "STATPEARLS_MAP" },
|
||||
{ label: "Resting band", low: 70, high: 100, citation: "STATPEARLS_MAP", normal: true },
|
||||
],
|
||||
referenceId: "STATPEARLS_MAP",
|
||||
guidanceCaveat:
|
||||
@@ -142,7 +157,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
PULSE_WAVE_VELOCITY: {
|
||||
unit: "m/s",
|
||||
bands: [
|
||||
{ label: "Reference", high: 10, citation: "ESC_ESH_2018_PWV" },
|
||||
{ label: "Reference", high: 10, citation: "ESC_ESH_2018_PWV", normal: true },
|
||||
{ label: "Elevated stiffness", low: 10, citation: "ESC_ESH_2018_PWV" },
|
||||
],
|
||||
referenceId: "ESC_ESH_2018_PWV",
|
||||
@@ -151,14 +166,14 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
},
|
||||
|
||||
// ── Resting heart rate — uses RESTING_HEART_RATE, never workout pulse. ──
|
||||
// Bands are authored normal-first so `classifyReference` treats the
|
||||
// population-normal band as the "within" placement. The athletic band
|
||||
// The 60–100 band carries `normal: true`, so `classifyReference` treats it
|
||||
// as the "within" placement regardless of band order. The athletic band
|
||||
// (40–60) is benign for a trained heart but sits below the standard
|
||||
// normal range — the caveat copy carries that nuance.
|
||||
RESTING_HEART_RATE: {
|
||||
unit: "bpm",
|
||||
bands: [
|
||||
{ label: "Normal adult resting", low: 60, high: 100, citation: "AHA_2024_RHR" },
|
||||
{ label: "Normal adult resting", low: 60, high: 100, citation: "AHA_2024_RHR", normal: true },
|
||||
{ label: "Athletic (benign)", low: 40, high: 60, citation: "AHA_2024_RHR" },
|
||||
],
|
||||
referenceId: "AHA_2024_RHR",
|
||||
@@ -178,7 +193,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
OXYGEN_SATURATION: {
|
||||
unit: "%",
|
||||
bands: [
|
||||
{ label: "Healthy room air", low: 95, high: 100, citation: "STATPEARLS_PULSE_OX" },
|
||||
{ label: "Healthy room air", low: 95, high: 100, citation: "STATPEARLS_PULSE_OX", normal: true },
|
||||
{ label: "Mild hypoxaemia", low: 91, high: 94, citation: "STATPEARLS_PULSE_OX" },
|
||||
],
|
||||
referenceId: "STATPEARLS_PULSE_OX",
|
||||
@@ -189,7 +204,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
RESPIRATORY_RATE: {
|
||||
unit: "breaths/min",
|
||||
bands: [
|
||||
{ label: "Normal resting adult", low: 12, high: 20, citation: "ALA_RESPIRATORY_RATE" },
|
||||
{ label: "Normal resting adult", low: 12, high: 20, citation: "ALA_RESPIRATORY_RATE", normal: true },
|
||||
{ label: "Worth attention", low: 22, citation: "RCP_2017_NEWS2" },
|
||||
],
|
||||
referenceId: "ALA_RESPIRATORY_RATE",
|
||||
@@ -201,7 +216,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
BODY_TEMPERATURE: {
|
||||
unit: "°C",
|
||||
bands: [
|
||||
{ label: "Normal oral range", low: 35.7, high: 37.4, citation: "JGIM_2019_TEMPERATURE" },
|
||||
{ label: "Normal oral range", low: 35.7, high: 37.4, citation: "JGIM_2019_TEMPERATURE", normal: true },
|
||||
{ label: "Fever", low: 38.0, citation: "JGIM_2019_TEMPERATURE" },
|
||||
],
|
||||
referenceId: "JGIM_2019_TEMPERATURE",
|
||||
@@ -213,7 +228,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
BLOOD_GLUCOSE: {
|
||||
unit: "mg/dL",
|
||||
bands: [
|
||||
{ label: "Normal fasting", high: 100, citation: "ADA_2024_GLYCEMIC" },
|
||||
{ label: "Normal fasting", high: 100, citation: "ADA_2024_GLYCEMIC", normal: true },
|
||||
{ label: "Prediabetes (fasting)", low: 100, high: 125, citation: "ADA_2024_GLYCEMIC" },
|
||||
{ label: "Diabetes range (fasting)", low: 126, citation: "ADA_2024_GLYCEMIC" },
|
||||
],
|
||||
@@ -231,7 +246,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
HBA1C: {
|
||||
unit: "%",
|
||||
bands: [
|
||||
{ label: "Normal", high: 5.7, citation: "ADA_2024_GLYCEMIC" },
|
||||
{ label: "Normal", high: 5.7, citation: "ADA_2024_GLYCEMIC", normal: true },
|
||||
{ label: "Prediabetes", low: 5.7, high: 6.4, citation: "ADA_2024_GLYCEMIC" },
|
||||
{ label: "Diabetes", low: 6.5, citation: "ADA_2024_GLYCEMIC" },
|
||||
],
|
||||
@@ -244,7 +259,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
unit: "kg/m²",
|
||||
bands: [
|
||||
{ label: "Underweight", high: 18.5, citation: "WHO_2020_PA" },
|
||||
{ label: "Normal", low: 18.5, high: 24.9, citation: "WHO_2020_PA" },
|
||||
{ label: "Normal", low: 18.5, high: 24.9, citation: "WHO_2020_PA", normal: true },
|
||||
{ label: "Overweight", low: 25.0, high: 29.9, citation: "WHO_2020_PA" },
|
||||
{ label: "Obesity", low: 30.0, citation: "WHO_2020_PA" },
|
||||
],
|
||||
@@ -257,7 +272,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
STEPS: {
|
||||
unit: "steps/day",
|
||||
bands: [
|
||||
{ label: "Higher mortality benefit", low: 8000, citation: "STEPS_SAINT_MAURICE_2020" },
|
||||
{ label: "Higher mortality benefit", low: 8000, citation: "STEPS_SAINT_MAURICE_2020", normal: true },
|
||||
],
|
||||
referenceId: "STEPS_SAINT_MAURICE_2020",
|
||||
guidanceCaveat:
|
||||
@@ -267,7 +282,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
VISCERAL_FAT: {
|
||||
unit: "rating",
|
||||
bands: [
|
||||
{ label: "Healthy rating", low: 1, high: 12, citation: "VAT_IMAGING_THRESHOLD" },
|
||||
{ label: "Healthy rating", low: 1, high: 12, citation: "VAT_IMAGING_THRESHOLD", normal: true },
|
||||
{ label: "Elevated rating", low: 13, citation: "VAT_IMAGING_THRESHOLD" },
|
||||
],
|
||||
referenceId: "VAT_IMAGING_THRESHOLD",
|
||||
@@ -279,7 +294,7 @@ export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
|
||||
SLEEP_DURATION: {
|
||||
unit: "h",
|
||||
bands: [
|
||||
{ label: "Adult 18–64", low: 7, high: 9, citation: "AASM_2015_SLEEP" },
|
||||
{ label: "Adult 18–64", low: 7, high: 9, citation: "AASM_2015_SLEEP", normal: true },
|
||||
{ label: "Older adult 65+", low: 7, high: 8, citation: "AASM_2015_SLEEP" },
|
||||
],
|
||||
referenceId: "AASM_2015_SLEEP",
|
||||
@@ -337,6 +352,20 @@ export type ReferencePlacement =
|
||||
* baseline). Pure + side-effect free; the user's own baseline still leads
|
||||
* the read — this is only a coarse placement aid.
|
||||
*/
|
||||
/**
|
||||
* Resolve the index of a metric's population-normal band — the band the
|
||||
* placement contract treats as "within". Returns the index of the band
|
||||
* flagged `normal: true`; falls back to band 0 only for the legacy
|
||||
* (untagged) shape so an unmarked metric degrades to the old normal-first
|
||||
* assumption rather than throwing. A metric with no bands returns -1.
|
||||
*/
|
||||
export function normalBandIndex(metric: ReferenceMetric): number {
|
||||
const bands = REFERENCE_RANGES[metric].bands;
|
||||
if (bands.length === 0) return -1;
|
||||
const marked = bands.findIndex((b) => b.normal === true);
|
||||
return marked === -1 ? 0 : marked;
|
||||
}
|
||||
|
||||
export function classifyReference(
|
||||
metric: ReferenceMetric,
|
||||
value: number | null | undefined,
|
||||
@@ -345,26 +374,26 @@ export function classifyReference(
|
||||
if (value == null || !Number.isFinite(value) || range.bands.length === 0) {
|
||||
return "insufficient";
|
||||
}
|
||||
// Find the first band whose half-open interval contains the value.
|
||||
// Bands are authored in ascending order; the FIRST band is the
|
||||
// normal/good placement, so its membership maps to `within`.
|
||||
const normalIdx = normalBandIndex(metric);
|
||||
// Find the band whose half-open interval contains the value. Bands are
|
||||
// authored in ascending order; the placement is the band's DISTANCE from
|
||||
// the normal band (resolved by the `normal` marker, NOT by index 0 —
|
||||
// some metrics author an abnormal band first, e.g. BMI Underweight,
|
||||
// pulse-pressure Narrow). One step from normal = watch tier; two or more
|
||||
// = attention tier.
|
||||
for (let i = 0; i < range.bands.length; i++) {
|
||||
const band = range.bands[i];
|
||||
const aboveLow = band.low == null || value >= band.low;
|
||||
const belowHigh = band.high == null || value <= band.high;
|
||||
if (aboveLow && belowHigh) {
|
||||
if (i === 0) return "within";
|
||||
// The band immediately adjacent to normal is the watch tier; any
|
||||
// band beyond it is the attention tier.
|
||||
return i === 1 ? "slightly-outside" : "outside";
|
||||
const distance = Math.abs(i - normalIdx);
|
||||
if (distance === 0) return "within";
|
||||
return distance === 1 ? "slightly-outside" : "outside";
|
||||
}
|
||||
}
|
||||
// Value sits beyond every authored band (e.g. below an open-low normal,
|
||||
// or above the top band's open-high). Treat an under-shoot of the first
|
||||
// band's low as within when the first band is open-low; otherwise the
|
||||
// value is outside the covered range → attention.
|
||||
const first = range.bands[0];
|
||||
if (first.low != null && value < first.low) return "outside";
|
||||
// Value sits beyond every authored band (below an open-low first band, or
|
||||
// above an open-high last band). Either way it is outside the covered
|
||||
// bands → attention.
|
||||
return "outside";
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user