Skip to content

Commit cd738f1

Browse files
committed
feat(yaxis): add alignZero to share zero baseline across multiple y-axes
1 parent 350ed2e commit cd738f1

1 file changed

Lines changed: 100 additions & 32 deletions

File tree

src/modules/Scales.js

Lines changed: 100 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -852,51 +852,119 @@ export default class Scales {
852852
}
853853
})
854854

855-
// Second pass: align the y=0 pixel position across opted-in axes by picking
856-
// R* = max(R_i) where R_i = -minY_i / (maxY_i - minY_i). The max-ratio
857-
// anchor never clips a mixed-sign axis's negative range; positive-leaning
858-
// axes get their min extended downward to match. With unified R*, the
859-
// downstream baseLineY[i] computation in CoreUtils.getCalculatedRatios()
860-
// yields the same pixel offset for every participant.
855+
// Second pass: align the y=0 pixel position across opted-in axes. We run
856+
// niceScale first on each participant's natural bounds so the tick step
857+
// and rounding are honored, then extend the bottom (or top) tick(s) of
858+
// each axis so the post-niceScale ratio matches R* — defined as the max
859+
// of natural ratios so no mixed-sign axis loses its negative range.
861860
if (alignZeroParticipants.length >= 2) {
861+
// Pass 2a: run niceScale on each participant's natural bounds.
862+
alignZeroParticipants.forEach((p) => {
863+
this.setYScaleForIndex(p.ai, p.minY, p.maxY)
864+
axisSeriesMap[p.ai].forEach((si) => {
865+
minYArr[si] = gl.yAxisScale[p.ai].niceMin
866+
maxYArr[si] = gl.yAxisScale[p.ai].niceMax
867+
})
868+
})
869+
870+
// Pass 2b: target ratio from post-niceScale bounds (niceScale may have
871+
// rounded values outward, so natural ratios are no longer reliable).
862872
let targetRatio = 0
863873
alignZeroParticipants.forEach((p) => {
864-
const range = p.maxY - p.minY
874+
const scale = gl.yAxisScale[p.ai]
875+
const range = scale.niceMax - scale.niceMin
865876
if (range > 0) {
866-
const r = -p.minY / range
877+
const r = -scale.niceMin / range
867878
if (r > targetRatio) targetRatio = r
868879
}
869880
})
870881
if (targetRatio > 1) targetRatio = 1
871882
if (targetRatio < 0) targetRatio = 0
872883

884+
// Pass 2c: rebuild each participant's tick array with a step sized to
885+
// the new range so we keep tick counts reasonable AND land close to R*.
886+
// For each axis we (a) compute the exact target bound that satisfies R*,
887+
// (b) pick a nice step that yields roughly the original tick count over
888+
// the new range, then (c) snap both bounds to step multiples. The
889+
// residual ratio drift is bounded by one step / new-range.
890+
/** @param {number} raw */
891+
const niceRoundUp = (raw) => {
892+
if (raw <= 0) return 1
893+
const mag = Math.floor(Math.log10(raw))
894+
const magPow = Math.pow(10, mag)
895+
const msd = raw / magPow // in [1, 10)
896+
let mult
897+
if (msd <= 1 + 1e-9) mult = 1
898+
else if (msd <= 2 + 1e-9) mult = 2
899+
else if (msd <= 2.5 + 1e-9) mult = 2.5
900+
else if (msd <= 5 + 1e-9) mult = 5
901+
else mult = 10
902+
return mult * magPow
903+
}
904+
873905
alignZeroParticipants.forEach((p) => {
874-
let adjMinY = p.minY
875-
let adjMaxY = p.maxY
876-
const range = p.maxY - p.minY
877-
const r = range > 0 ? -p.minY / range : 0
878-
if (Math.abs(r - targetRatio) > 1e-12) {
879-
if (targetRatio < 1 - 1e-12 && targetRatio > 1e-12) {
880-
// Extend the side that doesn't carry actual data so we never clip
881-
// real values. Positive-leaning axes grow downward; negative-
882-
// leaning ones grow upward.
883-
if (r < targetRatio) {
884-
adjMinY = (-targetRatio * p.maxY) / (1 - targetRatio)
885-
} else {
886-
adjMaxY = (-p.minY * (1 - targetRatio)) / targetRatio
887-
}
888-
} else if (targetRatio >= 1 - 1e-12) {
889-
// All-negative anchor — pin max at zero.
890-
adjMaxY = 0
891-
} else {
892-
// targetRatio ~= 0, all-positive anchor — pin min at zero.
893-
adjMinY = 0
894-
}
906+
const scale = gl.yAxisScale[p.ai]
907+
if (!scale.result || scale.result.length < 2) return
908+
const range = scale.niceMax - scale.niceMin
909+
if (range <= 0) return
910+
const r = -scale.niceMin / range
911+
if (Math.abs(r - targetRatio) <= 1e-9) return
912+
913+
// Direction: positive-leaning (r < R*) extends min down,
914+
// negative-leaning (r > R*) extends max up.
915+
const extendMin = r < targetRatio && targetRatio < 1 - 1e-9
916+
const extendMaxOnly = !extendMin && r > targetRatio && targetRatio > 1e-9
917+
if (!extendMin && !extendMaxOnly) return
918+
919+
const targetNiceMin = extendMin
920+
? (-targetRatio * scale.niceMax) / (1 - targetRatio)
921+
: scale.niceMin
922+
const targetNiceMax = extendMaxOnly
923+
? (-scale.niceMin * (1 - targetRatio)) / targetRatio
924+
: scale.niceMax
925+
926+
const newRange = targetNiceMax - targetNiceMin
927+
if (newRange <= 0) return
928+
const desiredTicks = Math.max(scale.result.length, 5)
929+
const newStep = niceRoundUp(newRange / Math.max(desiredTicks - 1, 1))
930+
if (newStep <= 0) return
931+
932+
let newNiceMin
933+
let newNiceMax
934+
if (extendMin) {
935+
// Snap min down to a step multiple, then derive max from R* so
936+
// alignment stays exact. Grow max further if data demands it.
937+
newNiceMin = Math.floor(targetNiceMin / newStep + 1e-9) * newStep
938+
const requiredMax =
939+
targetRatio > 1e-9
940+
? (newNiceMin * (targetRatio - 1)) / targetRatio
941+
: scale.niceMax
942+
const maxNeeded = Math.max(requiredMax, scale.niceMax)
943+
newNiceMax = Math.ceil(maxNeeded / newStep - 1e-9) * newStep
944+
} else {
945+
// Symmetric: snap max up, derive min from R*.
946+
newNiceMax = Math.ceil(targetNiceMax / newStep - 1e-9) * newStep
947+
const requiredMin =
948+
targetRatio < 1 - 1e-9
949+
? (-targetRatio * newNiceMax) / (1 - targetRatio)
950+
: scale.niceMin
951+
const minNeeded = Math.min(requiredMin, scale.niceMin)
952+
newNiceMin = Math.floor(minNeeded / newStep + 1e-9) * newStep
895953
}
896-
this.setYScaleForIndex(p.ai, adjMinY, adjMaxY)
954+
955+
scale.result = []
956+
for (
957+
let v = newNiceMin;
958+
v <= newNiceMax + newStep * 1e-9;
959+
v = Utils.preciseAddition(v, newStep)
960+
) {
961+
scale.result.push(Utils.stripNumber(v, 7))
962+
}
963+
scale.niceMin = newNiceMin
964+
scale.niceMax = newNiceMax
897965
axisSeriesMap[p.ai].forEach((si) => {
898-
minYArr[si] = gl.yAxisScale[p.ai].niceMin
899-
maxYArr[si] = gl.yAxisScale[p.ai].niceMax
966+
minYArr[si] = scale.niceMin
967+
maxYArr[si] = scale.niceMax
900968
})
901969
})
902970
} else if (alignZeroParticipants.length === 1) {

0 commit comments

Comments
 (0)