fix(B05,B06): 유토곡선이 그래프 칸 밖으로 새던 것 차단
사용자 보고(2026-09-04) — 「0선이 갑자기 위로 넘어가고 그 영역 채움색이 눈금 상한을 넘어 표현됨」. 보이는 구간에 세로를 맞추면서 0이 창 밖으로 나가는데, 유토곡선에는 **자르는 영역(clip)이 없어** 0선과 면 채움색이 범례·토글 버튼 위까지 그대로 칠해짐. 종단 그래프는 진작 자르고 있었는데 유토곡선만 빠져 있었음. - 그래프 칸(축 오른쪽 · 상단 여백 아래)을 자르는 겹 둘을 둠 — 측점 세로선이 면과 곡선 **사이**에 서야 해서 앞뒤로 나눔(그리는 순서 유지). - 면·곡선·0선·EP 표식·선택 강조·말풍선·토량 분배를 그 겹 안으로 옮김. - 0선은 창 안에 있을 때만 그리고 눈금에도 그때만 실음 — 밖으로 나갔는데 실으면 좌측 고정 축 맨 위에 남의 값과 겹쳐 찍힘(사용자 확정: 0선은 나가도 됨). - 눈금 글자는 축 **왼쪽**이라 자르는 겹에 넣지 않음(넣으면 사라짐). 자체검증(공용 브라우저 임시 탭): 확대 4칸 + 휠 25칸으로 0을 창 밖으로 보낸 상태에서 ① 자르는 겹 2개 확인 ② 0선·0눈금 사라짐, 눈금 5개 전부 음수(겹쳐 찍힌 0 없음) ③ 영역 캡처로 면 채움색이 그래프 칸 위 경계에서 정확히 멈추는 것 확인. 전체 시험 381 통과·17 건너뜀. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -442,7 +442,14 @@ function appendTransfer(
|
||||
* 띠 면을 누르면 그 balloon이 강조된다. 다른 띠나 빈 곳을 누르면 풀린다
|
||||
* (2026-08-02 사용자 지시). 강조는 이 SVG 안에서만 사는 상태라 다시 그리면 초기화된다.
|
||||
*/
|
||||
export function appendBalanceLayer(svg: SVGSVGElement, plan: HaulPlan, box: BalanceLayerBox): void {
|
||||
export function appendBalanceLayer(
|
||||
svg: SVGSVGElement,
|
||||
plan: HaulPlan,
|
||||
box: BalanceLayerBox,
|
||||
/** 실제로 붙일 자리 — 그래프 칸 밖을 자르는 겹이 있으면 그쪽(2026-09-04). 재는 일은
|
||||
* `svg`(뷰박스가 필요)가 그대로 맡는다. */
|
||||
host: SVGSVGElement | SVGGElement = svg,
|
||||
): void {
|
||||
const group = svgElement("g", { class: "b06-balance" });
|
||||
appendBalanceLine(group, plan, box);
|
||||
|
||||
@@ -482,7 +489,7 @@ export function appendBalanceLayer(svg: SVGSVGElement, plan: HaulPlan, box: Bala
|
||||
balloons.forEach((balloon, index) => bind(balloon, index));
|
||||
// 띠 밖(그래프 빈 곳)을 누르면 강조를 푼다.
|
||||
svg.addEventListener("click", clearAll);
|
||||
svg.append(group);
|
||||
host.append(group);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -241,7 +241,8 @@ function textWidth(value: string): number {
|
||||
* 재서 여유가 큰 쪽을 고르며, 어느 쪽에도 안 들어가면 여유가 더 큰 쪽 끝에 붙인다.
|
||||
*/
|
||||
function appendVolumeCallout(
|
||||
svg: SVGSVGElement,
|
||||
/** 붙일 자리 — 그래프 칸 밖을 자르는 겹이다(2026-09-04). */
|
||||
svg: SVGSVGElement | SVGGElement,
|
||||
point: MassHaulPoint,
|
||||
stationInterval: number,
|
||||
box: CalloutPlacement,
|
||||
@@ -368,6 +369,27 @@ export function createMassHaulChart(
|
||||
const span = Math.max(max - min, 1e-6);
|
||||
const y = (volume: number) => padTop + ((max - volume) / span) * plotHeight;
|
||||
|
||||
// 그래프 칸 밖으로 나간 것은 그리지 않는다(2026-09-04 사용자 보고) — 보이는 구간에
|
||||
// 세로를 맞추면 0선과 그 채움색이 눈금 상한을 넘어 **범례·토글 버튼 위까지** 칠해졌다.
|
||||
// 종단 그래프는 진작 자르고 있었는데 유토곡선만 빠져 있었다.
|
||||
const plotBottom = heightPx - MASS_PAD_BOTTOM;
|
||||
const clipId = `b06-mass-clip-${Math.random().toString(36).slice(2, 9)}`;
|
||||
const clip = svgElement("clipPath", { id: clipId });
|
||||
clip.append(
|
||||
svgElement("rect", {
|
||||
x: axisX,
|
||||
y: padTop,
|
||||
width: Math.max(0, widthPx - axis.padRight - axisX),
|
||||
height: Math.max(0, plotHeight),
|
||||
}),
|
||||
);
|
||||
const defs = svgElement("defs", {});
|
||||
defs.append(clip);
|
||||
// 두 겹으로 나누는 이유: 측점 세로선이 면과 곡선 **사이**에 서야 한다(그리는 순서 유지).
|
||||
const plotBack = svgElement("g", { "clip-path": `url(#${clipId})` });
|
||||
const plotFront = svgElement("g", { "clip-path": `url(#${clipId})` });
|
||||
svg.append(defs);
|
||||
|
||||
// 패널을 줄이면 그래프 몫이 60px대까지 내려간다 — 눈금 5개를 그대로 두면 라벨이 서로 겹친다.
|
||||
const tickRatios = plotHeight < 110 ? [0, 0.5, 1] : [0, 0.25, 0.5, 0.75, 1];
|
||||
const axisTicks: Array<{ y: number; label: string }> = [];
|
||||
@@ -395,8 +417,10 @@ export function createMassHaulChart(
|
||||
}
|
||||
|
||||
const zeroY = y(0);
|
||||
// 0선은 격자보다 진하게 따로 그리므로 고정 축 눈금에도 함께 실어 준다.
|
||||
axisTicks.push({ y: zeroY, label: "0㎥" });
|
||||
// 0선이 창 안에 있을 때만 눈금에 싣는다 — 밖으로 나갔는데도 실으면 고정 축 맨 위에
|
||||
// 남의 값과 겹쳐 찍힌다(2026-09-04 사용자 보고).
|
||||
const zeroInside = zeroY >= padTop && zeroY <= plotBottom;
|
||||
if (zeroInside) axisTicks.push({ y: zeroY, label: "0㎥" });
|
||||
onAxis?.({ padLeft: axisX, ticks: axisTicks });
|
||||
const visible = series.filter((entry) => visibleKeys.has(entry.key));
|
||||
|
||||
@@ -420,7 +444,7 @@ export function createMassHaulChart(
|
||||
const banded = visible[0];
|
||||
if (banded) {
|
||||
const last = banded.result.points[banded.result.points.length - 1];
|
||||
svg.append(
|
||||
plotBack.append(
|
||||
svgElement("path", {
|
||||
// 곡선과 **같은 경로**로 위쪽 테두리를 그린 뒤 0선을 따라 닫는다.
|
||||
d:
|
||||
@@ -431,6 +455,8 @@ export function createMassHaulChart(
|
||||
);
|
||||
}
|
||||
|
||||
svg.append(plotBack);
|
||||
|
||||
// 측점선 — 종단도와 같은 자리에 서고, 눌러서 측점을 고를 수 있다(3자 선택 동기화).
|
||||
// 라벨은 바로 위 종단면도가 이미 달고 있어 여기서는 생략한다(중복 표기 방지).
|
||||
for (const station of longitudinal.stations) {
|
||||
@@ -465,27 +491,33 @@ export function createMassHaulChart(
|
||||
svg.append(marker);
|
||||
}
|
||||
|
||||
// 0선 — 절토 우세와 성토 우세를 가르는 기준선이라 격자보다 진하게 그리고 눈금값도 따로 적는다.
|
||||
// Y 범위 계산(`volumeRange`)이 0을 항상 품으므로 이 선은 어떤 노선에서도 화면에 남는다.
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
x1: axisX,
|
||||
y1: zeroY,
|
||||
x2: widthPx - axis.padRight,
|
||||
y2: zeroY,
|
||||
class: "b06-masshaul__zero",
|
||||
}),
|
||||
svgText("0", {
|
||||
x: axisX - 9,
|
||||
y: zeroY + 4,
|
||||
"text-anchor": "end",
|
||||
class: "b06-chart__tick b06-masshaul__zero-tick",
|
||||
}),
|
||||
);
|
||||
// 0선 — 절토 우세와 성토 우세를 가르는 기준선이라 격자보다 진하게 그린다.
|
||||
// 보이는 구간에 세로를 맞추면 0이 창 밖으로 나갈 수 있다(2026-09-04 사용자 확정:
|
||||
// 「나가도 됨」) — 그때는 아예 그리지 않는다. 글자는 축 **왼쪽**이라 자르는 겹에 넣으면
|
||||
// 사라지므로 따로 붙인다.
|
||||
if (zeroInside) {
|
||||
plotFront.append(
|
||||
svgElement("line", {
|
||||
x1: axisX,
|
||||
y1: zeroY,
|
||||
x2: widthPx - axis.padRight,
|
||||
y2: zeroY,
|
||||
class: "b06-masshaul__zero",
|
||||
}),
|
||||
);
|
||||
svg.append(
|
||||
svgText("0", {
|
||||
x: axisX - 9,
|
||||
y: zeroY + 4,
|
||||
"text-anchor": "end",
|
||||
class: "b06-chart__tick b06-masshaul__zero-tick",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 뒤에 그린 곡선이 위로 오므로 범례 순서의 역순으로 그려 첫 곡선(정식)을 가장 위에 둔다.
|
||||
for (const entry of [...visible].reverse()) {
|
||||
svg.append(
|
||||
plotFront.append(
|
||||
svgElement("path", {
|
||||
d: curvePath(entry.result.points, x, y),
|
||||
class: seriesClass(entry, "b06-masshaul__curve"),
|
||||
@@ -504,7 +536,7 @@ export function createMassHaulChart(
|
||||
// 종점은 오른쪽 끝이라 라벨을 왼쪽으로 눕히고, 위아래는 플롯 안으로 클램프한다.
|
||||
const labelX = Math.max(epX - 8, axis.padLeft + labelWidth);
|
||||
const labelY = Math.min(Math.max(epY - 8, padTop + 12), heightPx - MASS_PAD_BOTTOM - 4);
|
||||
svg.append(
|
||||
plotFront.append(
|
||||
svgElement("circle", { cx: epX, cy: epY, r: 3.5, class: "b06-masshaul__ep-dot" }),
|
||||
svgElement("rect", {
|
||||
x: labelX - labelWidth,
|
||||
@@ -527,23 +559,29 @@ export function createMassHaulChart(
|
||||
// 토량 분배 레이어 — 곡선 **위**, 선택 말풍선 **아래**. 순서를 바꾸면 평형선이 곡선을
|
||||
// 가리거나(위로 올리면) 선택 말풍선이 balloon에 묻힌다(아래로 내리면).
|
||||
if (haulPlan) {
|
||||
appendBalanceLayer(svg, haulPlan, {
|
||||
x,
|
||||
y,
|
||||
left: axis.padLeft,
|
||||
right: widthPx - axis.padRight,
|
||||
top: padTop,
|
||||
bottom: heightPx - MASS_PAD_BOTTOM,
|
||||
curveYAt,
|
||||
stationInterval,
|
||||
});
|
||||
appendBalanceLayer(
|
||||
svg,
|
||||
haulPlan,
|
||||
{
|
||||
x,
|
||||
y,
|
||||
left: axis.padLeft,
|
||||
right: widthPx - axis.padRight,
|
||||
top: padTop,
|
||||
bottom: heightPx - MASS_PAD_BOTTOM,
|
||||
curveYAt,
|
||||
stationInterval,
|
||||
},
|
||||
// 그리는 자리는 자르는 겹 — 창 밖으로 나간 평형선·말풍선이 눈금 위로 새지 않는다.
|
||||
plotFront,
|
||||
);
|
||||
}
|
||||
|
||||
// 선택 측점 강조 — 곡선 위 점 + 구간 물량 말풍선.
|
||||
const focus = banded ? massHaulPointAt(banded, longitudinal, selectedStationId) : null;
|
||||
if (focus) {
|
||||
const focusX = x(focus.chainage_m);
|
||||
svg.append(
|
||||
plotFront.append(
|
||||
svgElement("circle", {
|
||||
cx: focusX,
|
||||
cy: y(focus.cumulative_volume_m3),
|
||||
@@ -551,7 +589,7 @@ export function createMassHaulChart(
|
||||
class: "b06-masshaul__focus",
|
||||
}),
|
||||
);
|
||||
appendVolumeCallout(svg, focus, stationInterval, {
|
||||
appendVolumeCallout(plotFront, focus, stationInterval, {
|
||||
anchorX: focusX,
|
||||
anchorY: y(focus.cumulative_volume_m3),
|
||||
left: axis.padLeft,
|
||||
@@ -562,6 +600,8 @@ export function createMassHaulChart(
|
||||
});
|
||||
}
|
||||
|
||||
svg.append(plotFront);
|
||||
|
||||
svg.append(
|
||||
svgElement("line", {
|
||||
x1: axisX,
|
||||
|
||||
Reference in New Issue
Block a user