fix(B06): 횡단면도 Y축 눈금도 정수·1·2·5 계열로 정의
- 5등분 고정(소수 표기) → 종단면도와 같은 niceTickStep 규칙 적용. - niceTickStep을 _UI_Section_Common으로 옮겨 종·횡단이 공유(정의 한 곳). - 횡단은 카드 높이가 낮아 라벨 간격 하한 14px(종단 16px)로 개수 상한 산정. - 횡단 표고 여유는 기존 crossPlotMetrics의 8% 패딩 그대로. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ import {
|
||||
type DesignChangeHandler,
|
||||
emptyView,
|
||||
L,
|
||||
niceTickStep,
|
||||
stationLabel,
|
||||
svgElement,
|
||||
svgText,
|
||||
@@ -317,9 +318,20 @@ export function createCrossSectionCard(
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Y축 눈금: 표고 범위를 10칸 안팎으로 나누되 눈금값이 1·2·5 계열로 딱 떨어지게 한다
|
||||
// (종단면도와 같은 규칙). 5등분 고정은 눈금값이 소수로 흘러 표고를 못 읽었다
|
||||
// (2026-08-23 사용자 지시). 라벨 겹침 방지로 눈금 간격은 14px 이상 띄운다.
|
||||
const rawDisplaySpan = displaySpan / exaggeration;
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
const tick = elevationMid - rawDisplaySpan / 2 + (rawDisplaySpan * index) / 4;
|
||||
const rawBottom = elevationMid - rawDisplaySpan / 2;
|
||||
const rawTop = elevationMid + rawDisplaySpan / 2;
|
||||
const plotHeightPx = heightPx - CROSS_PAD.top - CROSS_PAD.bottom;
|
||||
const tickStep = niceTickStep(rawDisplaySpan, 10, Math.floor(plotHeightPx / 14));
|
||||
const tickDecimals = Math.max(0, Math.ceil(-Math.log10(tickStep) - 1e-9));
|
||||
for (
|
||||
let tick = Math.ceil(rawBottom / tickStep) * tickStep;
|
||||
tick <= rawTop + 1e-9;
|
||||
tick += tickStep
|
||||
) {
|
||||
const displayTick = elevationMid + (tick - elevationMid) * exaggeration;
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
@@ -329,7 +341,7 @@ export function createCrossSectionCard(
|
||||
y2: y(displayTick),
|
||||
class: "b06-chart__grid",
|
||||
}),
|
||||
svgText(tick.toFixed(1), {
|
||||
svgText(tick.toFixed(tickDecimals), {
|
||||
x: CROSS_PAD.left - 7,
|
||||
y: y(displayTick) + 3,
|
||||
"text-anchor": "end",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
LONG_PAD,
|
||||
LONG_WIDTH,
|
||||
longitudinalMaxChainage,
|
||||
niceTickStep,
|
||||
stationLabel,
|
||||
svgElement,
|
||||
svgText,
|
||||
@@ -30,30 +31,6 @@ function offsetStationLabel(chainageM: number, interval: number, stationOffset:
|
||||
return `${Number(number) + stationOffset}+${remainder}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 눈금 간격을 1·2·5 ×10^n 중에서 고른다 — 눈금값이 딱 떨어지면서 눈금 개수가
|
||||
* 목표(10칸)에 가장 가까운 것. 다만 라벨이 겹치지 않게 최대 개수(maxCount)를 넘지 않는다.
|
||||
*/
|
||||
function niceTickStep(span: number, targetCount: number, maxCount: number): number {
|
||||
if (!(span > 0)) return 1;
|
||||
const exponent = Math.floor(Math.log10(span / targetCount));
|
||||
let best = Math.pow(10, exponent + 2);
|
||||
let bestError = Infinity;
|
||||
for (const power of [exponent - 1, exponent, exponent + 1, exponent + 2]) {
|
||||
for (const mantissa of [1, 2, 5]) {
|
||||
const step = mantissa * Math.pow(10, power);
|
||||
const count = Math.floor(span / step) + 1;
|
||||
if (count > Math.max(2, maxCount)) continue;
|
||||
const error = Math.abs(count - targetCount);
|
||||
if (error < bestError) {
|
||||
bestError = error;
|
||||
best = step;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
export function longitudinalMinimumWidth(
|
||||
data: LongitudinalSection,
|
||||
configuredStationInterval?: number,
|
||||
|
||||
@@ -165,3 +165,27 @@ export function emptyView(message: string): HTMLElement {
|
||||
empty.textContent = message;
|
||||
return empty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 눈금 간격을 1·2·5 ×10^n 중에서 고른다 — 눈금값이 딱 떨어지면서 눈금 개수가
|
||||
* 목표(10칸)에 가장 가까운 것. 다만 라벨이 겹치지 않게 최대 개수(maxCount)를 넘지 않는다.
|
||||
*/
|
||||
export function niceTickStep(span: number, targetCount: number, maxCount: number): number {
|
||||
if (!(span > 0)) return 1;
|
||||
const exponent = Math.floor(Math.log10(span / targetCount));
|
||||
let best = Math.pow(10, exponent + 2);
|
||||
let bestError = Infinity;
|
||||
for (const power of [exponent - 1, exponent, exponent + 1, exponent + 2]) {
|
||||
for (const mantissa of [1, 2, 5]) {
|
||||
const step = mantissa * Math.pow(10, power);
|
||||
const count = Math.floor(span / step) + 1;
|
||||
if (count > Math.max(2, maxCount)) continue;
|
||||
const error = Math.abs(count - targetCount);
|
||||
if (error < bestError) {
|
||||
bestError = error;
|
||||
best = step;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user