2D viewer: intensity readout is computed from the vertical pointer coordinate (only updates on vertical movement)
メンテナーはふだん 3 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 2/5
- 見積もり時間
- 1〜3時間
- 初心者へのやさしさ
- 80/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- react, typescript
- 領域
- frontend
調査の方向性
src/component/2d/FooterBanner.tsxを、ポインター座標の処理に関する参照先としてissueが示しているsrc/component/2d/tools/XYLabelPointer.tsxと併せて読んでください。getXValueとgetYValueがメインのトラックと他のトラックについて、ポインター位置をどのように対応付けるかを確認してください。次に、等高線プロット上で水平方向に動かしたときにフッターの表示値が変化し、ポインターの下にある信号と一致することを確かめてください。
索引モデルが issue の本文から書いたものです。
説明
Summary
In the 2D viewer footer, the F2 value and the Intensity value are both derived from the pointer's vertical (y) pixel coordinate. Two consequences:
- The
Intensityreadout only changes when the pointer is moved vertically. Sweeping horizontally across the 2D contour plot changes nothing. - The reported value does not correspond to the signal under the pointer: the z column is sampled at
F2 = xScale.invert(pointer.y)instead ofF2 = xScale.invert(pointer.x), so the intensity comes from a different point of the spectrum than the one being pointed at.
The F1 readout and the on-canvas pointer label (XYLabelPointer) are correct, so the footer contradicts the crosshair/label drawn next to the cursor.
Evidence

How to reproduce
- Open a 2D spectrum (e.g. the JCAMP-DX sample used in the GIF above).
- Hover the pointer over the 2D contour area.
- Move the pointer horizontally over the peaks ->
Intensitydoes not change. - Move it vertically ->
Intensitychanges, and the value does not match the peak under the pointer.
Root cause
src/component/2d/FooterBanner.tsx (line 131):
const getXValue = (x: number | null = null) => {
if (!scaleX || !trackID) return 0;
if (trackID === 'MAIN') {
return scaleX.invert(x || position.y); // <-- BUG: should be position.x
}
// return if the trackID = "MAIN" | "TOP"
return scaleX.invert(x || position.x);
};
getZValue() (line 188) builds both z indices from these helpers:
const xIndex = Math.floor((getXValue() - minX) / xStep); // = f(position.y)
const yIndex = Math.floor((getYValue() - minY) / yStep); // = f(position.y)
return z[yIndex][xIndex];
and getYValue() for MAIN is scaleY.invert(position.y). So in the main 2D area both indices are functions of position.y alone, which is exactly the reported behaviour: the intensity is constant along horizontal movement and jumps with tiny vertical drift.
Regression
Introduced in a6098c8a ("fix: correct rendering of 1D traces over the 2D (#3870)", 2025-12-03). That commit rewrote the switch (trackID) in getXValue as if-statements and, while doing so, changed the MAIN branch from position.x to position.y:
- switch (trackID) {
- case LAYOUT.main:
- case LAYOUT.top: {
- return scaleX.invert(x || position.x);
- }
- case LAYOUT.left: {
- return scaleX.invert(x || position.y);
- }
+ if (!scaleX || !trackID) return 0;
+
+ if (trackID === 'MAIN') {
+ return scaleX.invert(x || position.y); // regression
+ }
Before that commit the MAIN branch was correct.
Reference implementation
src/component/2d/tools/XYLabelPointer.tsx (line 100) does it correctly:
const getXValue = (x = null) => {
switch (trackID) {
case LAYOUT.main:
case LAYOUT.top: {
return scaleX?.invert(x || position.x); // correct
}
case LAYOUT.left: {
return scaleX?.invert(x || position.y);
}
...
const getYValue = () => {
switch (trackID) {
case LAYOUT.main:
case LAYOUT.top: {
return scaleY?.invert(position.y); // correct
}
Suggested fix
if (trackID === LAYOUT.main) {
return scaleX.invert(x ?? position.x);
}
While there, the TOP / LEFT branches of getXValue / getYValue should be aligned with XYLabelPointer's switch: for LEFT, getXValue currently returns scaleX.invert(position.x) where scaleX is the 2D y scale (get2DYScale), i.e. the same class of coordinate mix-up.
Analysis of the GIF (frame by frame)
The GIF is 808x471, 64 frames. Measuring the pointer crosshair per frame:
- Between frames 30 and 51 the crosshair moves horizontally from x ~ 241 px to x ~ 309 px (~68 px), while y stays within 249-251 px.
- Over that same interval the footer shows
F2 = 0.68 ppmandF1 = 0.0 ppmunchanged, andIntensitychanges only when y drifts:
| frame | pointer y | Intensity |
|---|---|---|
| 30 | 249 | 3034415 |
| 34 | 251 | 3910041 |
| 42 | 250 | 3499514 |
| 50 | 248 | 2063091 |
So the intensity follows 3 px of vertical drift, not the 68 px horizontal sweep. With xDomain ~ [0.55, 0.82] mapped over the ~403 px plot width, dF2/dy ~ -6.7e-4 ppm/px, so a 3 px drift moves the sampled z column by a few bins - enough to change the readout.
Note that the F2 readout looks plausible in the GIF only by coincidence: the pointer's x and y are almost equal at the start of the sweep, so xScale.invert(position.y) happens to be close to xScale.invert(position.x).
Environment
- nmrium 3.1.0, commit
5989ec6d
- 主要言語
- TypeScript
- スター
- 84
- フォーク
- 35
- 平均マージ
- 4日 23時間
- マージ済み PR(30日)
- 7
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
cheminfo/nmrium のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 3 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 3 日以内に返信
-
難易度 4/5 3〜5日 初心者へのやさしさ 39/100
メンテナーはふだん 3 日以内に返信
-
cutting a spectrum and vertical scale of 1D対応中かも @hamed-musallam が 2 日前に担当しました。 オープン
cheminfo/nmrium#4424 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 3 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 45/100
cheminfo/nmrium#4422 · コメント 2 件 ·
メンテナーはふだん 3 日以内に返信
cheminfo/nmrium の issue をすべて見る
似ている issue
-
check:passed streams:add
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
S: triage
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
メンテナーはふだん 1 日以内に返信
-
iot client
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
Open-Locker/Open-Locker#365 ·
メンテナーはふだん 1 日以内に返信
-
bug enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 69/100
inmanta/web-console#7787 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
shoelace-style/webawesome#2979 ·
メンテナーはふだん 1 日以内に返信