fix(RangeWithValue): correct the display of degenerate ranges and add a logarithmic scale - #335
Merged
Merged
Conversation
… wrong Three cases where the component silently rendered a misleading picture. percentage() short-circuited to 50% whenever expectedMin equalled expectedMax, and it did so for every value including actualValue. A value outside such a range therefore sat directly on top of the boundary it violated. The scale is only truly degenerate when the value coincides with the boundary as well — otherwise getBufferedRange already spans boundary to value — so narrow the guard to that case via isScaleCollapsed. A zero-width range also drew both boundary labels and both range lines on top of each other, which rendered blurry. Draw the single boundary once, prefix it with "=" to say it is one permitted value rather than a span, and skip the mean, which repeats the same number. An expectedMax below expectedMin placed the max label at -200%, outside the container, while the rest of the scale looked healthy. No value can satisfy such a range, so report it in place instead of charting it. Labels are now centered with transform: translateX(-50%) rather than a fixed calc(% - 14px) offset, so centering follows the rendered text width instead of assuming a 28px label. The story's numeric controls change from range sliders with step 1 to number inputs, which could not represent the decimal values these cases are about. 🤖 Generated with Claude Code
…ition isRangeZero was evaluated twice per section, once for the boundary and once for the mean. 🤖 Generated with Claude Code
The test only pinned the boundary label. Asserting the value point too proves the scale actually spans, rather than that one element moved. 🤖 Generated with Claude Code
The mean was rendered with toFixed(2). For bounds of 0.04 and 0.15 it showed 0.10 while sitting at 0.095, so a value of 0.1 appeared next to the line instead of on it. The average of two two-decimal bounds needs three decimals, so two can never be right here. 🤖 Generated with Claude Code
Every comparison with NaN is false, so `actualValue < expectedMin || actualValue > expectedMax` reported false for NaN and the value rendered as a green in-range pill labelled NaN. An infinite bound produced NaN% positions for the same reason. Both cases now share the invalid-bounds message instead. 🤖 Generated with Claude Code
A range derived multiplicatively from a configured mean and a deviation factor has that mean as its geometric centre, not its arithmetic one. Bounds 0.038 and 0.152 come from a mean of 0.076, but (min + max) / 2 labels them 0.095 — a number that appears nowhere in the configuration. meanType defaults to arithmetic, so existing callers are unaffected. A geometric mean is undefined for a lower bound of zero or below, which now reports through the same inline message as the other invalid inputs instead of silently rendering zero. 🤖 Generated with Claude Code
A multiplicatively derived range cannot be drawn centred on a linear scale: between 0.038 and 0.152 the geometric mean 0.076 sits at a third of the width. Labelling it correctly was not enough, it still read as left of centre. Replaces meanType with scale, because both the mean and the positioning follow from it. On a logarithmic scale the midpoint of the drawn line is the geometric mean, so the mean stops being a second concept. The buffer moves into log space as well. Subtracting it in value space drops a small lower bound below zero, and Math.log of a negative number is NaN. Proximity colouring stays on the arithmetic range, so it keeps matching the NeMo evaluation. 🤖 Generated with Claude Code
The near-bound band was a fraction of the arithmetic span even on a logarithmic scale. Between 1 and 100 that made 5 a near-miss of the lower bound, four times the bound itself, while the same fraction at the upper end covered 90 to 100. The band is now a fraction of the span in scale space, so it is a distance on a linear scale and a ratio on a logarithmic one. Linear behaviour is unchanged: the projection is the identity and the span is the range it was before. Moves the scale into its own module. utils.ts had collected colouring, label width, float noise and scale arithmetic; with two strategies the arithmetic earns its own file, and it no longer has to import a type back out of index.tsx. 🤖 Generated with Claude Code
…unds A geometric mean is rarely representable as a decimal fraction, so withoutFloatingPointNoise had nothing to cut and labelled the mean of 0.038 and 0.153 as 0.0762495901628. Rounding happens in meanOfScale rather than in the label, because the mean also positions its own line. 🤖 Generated with Claude Code
Two bounds alone do not show whether the axis is linear or logarithmic. Ticks make it visible by comparison: evenly spaced when linear, crowding towards the lower bound when logarithmic. Which ticks carry a label follows a rule rather than the positions that happen to be free: every tick on a linear scale, and the first, second and fifth subdivision of each logarithmic decade. Only a collision with a bound, mean or measurement label suppresses one. 🤖 Generated with Claude Code
A tick label sits below the bar, the measurement above it, so the two do not compete for space and the measurement no longer suppresses a label. Which subdivisions carry a label depends on the span, because the gaps within a decade are fixed: 1 to 2 is 30% of it, 8 to 9 only 5%. A scale showing less than 1.5 decades stretches those gaps and fits 4 and 6 as well. A zero-width range gets no ticks at all — for the window 0.029 to 0.03 it produced six of them, labelled 0.0292 and the like. 🤖 Generated with Claude Code
A range spanning a factor of four, as a qPCR control ratio often does, got labels at 0.02, 0.03 and 0.04 between bounds of 0.012 and 0.048 -- three numbers crowding the row that already carries the bounds and the mean. Below one decade only the strokes remain, and they may sit denser because nothing has to fit between them. Above a decade every stroke now carries a label, so ticks no longer need a per-tick flag: whether a scale is labelled is a property of its span. 🤖 Generated with Claude Code
|
🎉 This PR is included in version 20.8.0-alpha.5 🎉 The release is available on: Your semantic-release bot 📦🚀 |
The bounds and the mean already carry the numbers. Tick labels competed with them for the same row, which is what the collision filter and the two subdivision sets existed for. Without labels the strokes may always sit dense. 🤖 Generated with Claude Code
|
🎉 This PR is included in version 20.8.0-alpha.6 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Drop the min/max parameters of getBufferedRange — the caller passed values the function derives itself. Let meanOfScale return a display-ready number for both scales so withoutFloatingPointNoise stays internal: an arithmetic mean keeps the decimals it needs, a geometric one follows its bounds. Cover meanOfScale directly, including bounds in exponential notation, where decimalsOf would otherwise report no decimals and round the label to zero. Sort out two stories that the argTypes controls already reach, and drop the duplicate assertion that unlabelled ticks stay unlabelled. 🤖 Generated with Claude Code
simbig
requested review from
mic-web
and
a lite review from Copilot
and removed request for
Copilot
September 18, 2026 09:36
mic-web
approved these changes
Sep 18, 2026
simbig
marked this pull request as ready for review
September 21, 2026 07:11
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 21, 2026
## [20.7.1](v20.7.0...v20.7.1) (2026-09-21) ### Bug Fixes * **RangeWithValue:** correct the display of degenerate ranges and add a logarithmic scale ([#335](#335)) ([15ceb0e](15ceb0e))
|
🎉 This PR is included in version 20.7.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 21, 2026
# [20.8.0-alpha.7](v20.8.0-alpha.6...v20.8.0-alpha.7) (2026-09-21) ### Bug Fixes * **RangeWithValue:** correct the display of degenerate ranges and add a logarithmic scale ([#335](#335)) ([15ceb0e](15ceb0e))
|
🎉 This PR is included in version 20.8.0-alpha.7 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RangeWithValuezeichnete bei degenerierten Eingaben ein irreführendes Bild und positionierte multiplikativ abgeleitete Bereiche auf einer linearen Skala. Der PR behebt die Darstellungsfehler, sichert sie mit Tests ab und ergänzt ein optionalesscale-Prop.Fünf Defekte:
expectedMin === expectedMaxließ jeden Wert auf 50 % sitzen, eine Verletzung war damit unsichtbar=-PrefixexpectedMax < expectedMinsetzte das Max-Label auf -200 %; jetzt eine Inline-FehlermeldungtoFixed(2)machte aus dem Mittelwert 0,095 die Anzeige 0,10, die dann neben ihrer eigenen Linie standNaN-Messwert erschien als in-range, weil jeder Vergleich mitNaNfalse istNeu ist
scale?: 'linear' | 'logarithmic', Defaultlinear— additiv, kein bestehender Aufruf ändert sich.Warum eine Kontroll-Ratio eine logarithmische Skala braucht
Die Grenzen entstehen aus Mittelwert und Abweichungsfaktor: 0,076 ÷ 2 = 0,038 und 0,076 × 2 = 0,152. Linear sitzt der geometrische Mittelwert 0,076 bei einem Drittel der Breite — richtig beschriftet, aber optisch links der Mitte. Logarithmisch ist die Mitte der Linie der geometrische Mittelwert.
Mit der Skala wandern auch Puffer und Nähe-Warnung:
bufferPercentageist eine Distanz beilinear, ein Verhältnis beilogarithmic. Der Puffer wird im Log-Raum gerechnet, weil eine kleine untere Grenze sonst unter null rutscht undMath.logdavonNaNist. Bei unterer Grenze oder Messwert ≤ 0 lehnt ein Guard die logarithmische Skala ab.isOutOfRangebleibt ein blanker Vergleich gegen die Grenzen, von der Skala unberührt.Ticks tragen keine Zahlen
Zwei Grenzwerte allein zeigen nicht, ob die Achse linear oder logarithmisch ist — an den Ticks wird es ablesbar: gleichmäßig verteilt bei
linear, zur unteren Grenze gedrängt beilogarithmic. Dafür braucht keiner von ihnen eine Beschriftung. Ein Bereich über den Faktor vier ist bei einer Kontroll-Ratio der Normalfall, und dort standen die Zahlen so dicht, dass die Zeile mit Min, Max und Mittelwert unlesbar wurde.Sichtbare Verhaltensänderung: das Mittelwert-Label
Es zeigt nicht mehr immer zwei Nachkommastellen, sondern die Stellen, die der Mittelwert braucht: 0,095 statt 0,10, aber auch 55 statt 55,00. Es gibt eine bekannte Aufrufstelle, und nichts parst das Label. Ein geometrischer Mittelwert wird dabei auf die Stellen seiner Grenzen gerundet, sonst steht bei 0,038 und 0,153 die Zahl 0,0762495901628 — eine Wurzel ist selten als Dezimalbruch darstellbar.
Bewusst nicht behoben
0.1 + 0.2gegen0.3), lösen die Invalid-Bounds-Meldung ausAbdeckung: 22 Unit-Tests, 8 Stories
scale.test.tsprüft die Tick-Werte und den Mittelwert beider Skalen,index.test.tsxdas Gerenderte: Zero-Width-Range, invertierte Grenzen,NaN-Guard, unbeschriftete Ticks, und bei identischen Grenzen 1–100 die Position des Messwerts 4 (30,1 % logarithmisch gegen 3,03 % linear) sowie die Nähe-Warnung bei Messwert 5 (linear gelb, logarithmisch grün).Stories:
Default,EqualBounds,EqualBoundsMet,InvalidBounds,LogarithmicScale,LogarithmicScaleSpanningDecades,LogarithmicScaleWithZeroBound,MissingMeasurement.Bleibt Draft bis das Review-Feedback da ist. Ein Pre-release steht auf dem
alpha-Tag.🤖 Generated with Claude Code