From 2cea6203be4f62c84673752829607c11f1bc9484 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 23 Aug 2026 17:49:28 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EC=A2=85=EB=8B=A8=20Y=EC=B6=95=20?= =?UTF-8?q?=EB=88=88=EA=B8=88=20=EC=A0=95=EC=88=98=ED=99=94=20+=20?= =?UTF-8?q?=EC=B5=9C=EB=8C=80=C2=B7=EC=B5=9C=EC=86=8C=20=ED=91=9C=EA=B3=A0?= =?UTF-8?q?=20=EC=97=AC=EC=9C=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 0.25 비율 고정 5눈금(소수 표기) → 1·2·5×10^n 중 10칸에 가장 가까운 간격으로 자동 선택. 눈금값이 딱 떨어져 표고를 읽을 수 있다. - 라벨 겹침 방지로 눈금 간격 16px 하한(plotHeight/16 개수 상한). - 자동 스케일(B05)은 표고 범위에 위아래 10%씩 여유 — 최고·최저점이 축선에 붙지 않는다. 공통 Y스케일(B06 yScaleOptions)은 불변. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Longitudinal.ts | 51 +++++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/B06_Section/B06_Section_UI_Longitudinal.ts b/B06_Section/B06_Section_UI_Longitudinal.ts index 77597232..33932b90 100644 --- a/B06_Section/B06_Section_UI_Longitudinal.ts +++ b/B06_Section/B06_Section_UI_Longitudinal.ts @@ -30,6 +30,30 @@ 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, @@ -202,7 +226,9 @@ export function createLongitudinalProfile( const plotHeight = heightPx - LONG_PAD.top - padBottom; const elevationSpan = yScaleOptions ? plotHeight / yScaleOptions.pixelsPerMeter - : Math.max(rawMax - rawMin, 1); + : // 자동 스케일(B05)은 최고·최저 표고가 위아래 축선에 딱 붙지 않게 10%씩 여유를 둔다 + // (2026-08-23 사용자 지시). 공통 Y스케일(B06 yScaleOptions)은 그대로 둔다. + Math.max(rawMax - rawMin, 1) * 1.2; const x = (chainage: number) => LONG_PAD.left + originOffsetPx + (chainage / maxChainage) * plotWidth; const xInverse = inverseOf(x, maxChainage); @@ -210,12 +236,23 @@ export function createLongitudinalProfile( LONG_PAD.top + ((elevationMid + elevationSpan / 2 - elevation) / elevationSpan) * plotHeight; const stationInterval = configuredStationInterval ?? inferStationInterval(data.stations); + // Y축 눈금: 화면에 담긴 표고 범위를 10칸 안팎으로 나누되, 눈금값이 1·2·5 계열의 + // 딱 떨어지는 수(범위가 아주 좁을 때만 소수)로 오게 한다 — 0.25 비율 고정 눈금은 + // 표고가 소수로 나와 어느 지점인지 못 읽었다(2026-08-23 사용자 보고). const yAxisTicks: Array<{ y: number; label: string }> = []; - for (const ratio of [0, 0.25, 0.5, 0.75, 1]) { - const gridY = LONG_PAD.top + ratio * plotHeight; - const displayed = elevationMid + elevationSpan / 2 - ratio * elevationSpan; - const rawValue = elevationMid + (displayed - elevationMid) / exaggeration; - yAxisTicks.push({ y: gridY, label: `${rawValue.toFixed(1)}m` }); + const rawSpan = elevationSpan / exaggeration; + const rawTop = elevationMid + rawSpan / 2; + // 라벨 글자(최대 13px, sticky 축)가 겹치지 않게 눈금 간격은 16px 이상 띄운다. + const tickStep = niceTickStep(rawSpan, 10, Math.floor(plotHeight / 16)); + const tickDecimals = Math.max(0, Math.ceil(-Math.log10(tickStep) - 1e-9)); + for ( + let value = Math.ceil((elevationMid - rawSpan / 2) / tickStep) * tickStep; + value <= rawTop + 1e-9; + value += tickStep + ) { + const gridY = LONG_PAD.top + ((rawTop - value) / rawSpan) * plotHeight; + const label = `${value.toFixed(tickDecimals)}m`; + yAxisTicks.push({ y: gridY, label }); svg.append( svgElement("line", { x1: LONG_PAD.left, @@ -224,7 +261,7 @@ export function createLongitudinalProfile( y2: gridY, class: "b06-chart__grid", }), - svgText(`${rawValue.toFixed(1)}m`, { + svgText(label, { x: LONG_PAD.left - 9, y: gridY + 4, "text-anchor": "end",