- UI_Prob(새 파일): 대표 단면 · 고른 비탈 · 조건 · 단면 물성 × c · φ · γ(+ 수위선 = 지하수위 이동 · r_u 갈래 = r_u) · 쓰기 · 분포 · 변동계수/σ · 절단 ±kσ · 임계면 고정 / 매회 탐색 · 방법 · LHS / MC · N · 시드 · c–φ 상관 · [기본값] · 입력 = sessionStorage 캐시(자동저장 없음) · 계산 = 서버(잠금판 없이 카드 안 진행률) - UI_ProbChart(새 파일): F < 1 빨강 히스토그램 · F = 1 · 기준 · 결정론 선 · 누적분포(Pf) · 민감도(범위 %) · 토네이도 - logic/E03_StabSection_Prob.ts: 변수 줄 · 기본값 후보(물성 덮어쓰기) · 요청 꼴 · 바꾼 줄 수 · 눈금 - Page: 보기 고름에 「확률 · 민감도」 · 문구 locale_e03 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
7.8 KiB
TypeScript
221 lines
7.8 KiB
TypeScript
/* =============================================================================
|
|
* E03_StabSection_UI_ProbChart.ts
|
|
* E03 「확률 · 민감도」 그래프 SVG(PLAN 537-1) — 안전율 히스토그램(F < 1 빨강 · F = 1 · 기준 선) ·
|
|
* 누적분포 · 민감도(가로 = 범위 % · 변수마다 선 · Slide2 Sensitivity Plot) · 토네이도(끝값 F 폭 순).
|
|
* ========================================================================== */
|
|
|
|
import { el } from "@ui/ui_template_elements";
|
|
import { t } from "@ui/ui_template_locale";
|
|
import { axisTicks, type ProbCurve, type ProbResult } from "./logic/E03_StabSection_Prob";
|
|
import { pointsText, type Point } from "./logic/E03_StabSection_Logic";
|
|
import { svgEl } from "./E03_StabSection_UI_Section";
|
|
|
|
const W = 380;
|
|
const H = 200;
|
|
const PAD = { l: 44, r: 12, t: 12, b: 28 };
|
|
const fmt = (v: number | null | undefined, d = 2): string =>
|
|
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
|
|
|
|
interface Frame {
|
|
svg: SVGSVGElement;
|
|
sx: (x: number) => number;
|
|
sy: (y: number) => number;
|
|
}
|
|
|
|
/** 축 틀 — 가로 · 세로 눈금 · 축 이름 */
|
|
function frame(
|
|
role: string,
|
|
xr: [number, number],
|
|
yr: [number, number],
|
|
xLabel: string,
|
|
yLabel: string,
|
|
pad = PAD,
|
|
yAxis = true,
|
|
): Frame {
|
|
const xt = axisTicks(xr[0], xr[1], 5);
|
|
const yt = axisTicks(yr[0], yr[1], 4);
|
|
const [x0, x1] = [xt[0], xt[xt.length - 1]];
|
|
const [y0, y1] = [yt[0], yt[yt.length - 1]];
|
|
const sx = (x: number): number =>
|
|
pad.l + ((x - x0) / Math.max(x1 - x0, 1e-9)) * (W - pad.l - pad.r);
|
|
const sy = (y: number): number =>
|
|
H - pad.b - ((y - y0) / Math.max(y1 - y0, 1e-9)) * (H - pad.t - pad.b);
|
|
const svg = svgEl("svg", { viewBox: `0 0 ${W} ${H}`, "data-role": role }) as SVGSVGElement;
|
|
const axis = svgEl("g", { class: "e03-axis" });
|
|
for (const v of yAxis ? yt : []) {
|
|
axis.append(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: sy(v), y2: sy(v) }));
|
|
axis.append(svgEl("text", { x: pad.l - 4, y: sy(v) + 4, "text-anchor": "end" }, String(v)));
|
|
}
|
|
for (const v of xt)
|
|
axis.append(svgEl("text", { x: sx(v), y: H - pad.b + 14, "text-anchor": "middle" }, String(v)));
|
|
axis.append(svgEl("text", { x: W - pad.r, y: H - 2, "text-anchor": "end" }, xLabel));
|
|
axis.append(svgEl("text", { x: 2, y: pad.t - 2 }, yLabel));
|
|
svg.append(axis);
|
|
return { svg, sx, sy };
|
|
}
|
|
|
|
function mark(f: Frame, x: number, cls: string, label: string): void {
|
|
f.svg.append(
|
|
svgEl("line", {
|
|
x1: f.sx(x),
|
|
x2: f.sx(x),
|
|
y1: PAD.t,
|
|
y2: H - PAD.b,
|
|
class: `e03-prob-mark ${cls}`,
|
|
}),
|
|
svgEl("text", { x: f.sx(x) + 3, y: PAD.t + 10, class: `e03-prob-mark__text ${cls}` }, label),
|
|
);
|
|
}
|
|
|
|
function box(role: string, svg: SVGSVGElement, legend?: HTMLElement): HTMLElement {
|
|
const host = el("div", { className: "e03-graph", attrs: { "data-role": `${role}-box` } });
|
|
host.append(svg as unknown as HTMLElement);
|
|
if (legend) host.append(legend);
|
|
return host;
|
|
}
|
|
|
|
/** 안전율 히스토그램 — 칸 개수 · F < 1 칸 빨강 · F = 1 · 기준 · 결정론 선 */
|
|
export function histogramChart(
|
|
res: ProbResult,
|
|
required: number | null,
|
|
fsDet: number | null,
|
|
): HTMLElement {
|
|
const h = res.stats.histogram;
|
|
if (!h) return el("div");
|
|
const { edges, counts } = h;
|
|
const xr: [number, number] = [
|
|
Math.min(edges[0], 1),
|
|
Math.max(edges[edges.length - 1], required ?? 0),
|
|
];
|
|
const f = frame("prob-hist", xr, [0, Math.max(...counts, 1)], "Fs", t("E03_Pb_Count"));
|
|
counts.forEach((n, i) => {
|
|
const [a, b] = [edges[i], edges[i + 1]];
|
|
const bar = svgEl("rect", {
|
|
x: f.sx(a),
|
|
y: f.sy(n),
|
|
width: Math.max(f.sx(b) - f.sx(a) - 0.5, 0.5),
|
|
height: f.sy(0) - f.sy(n),
|
|
class: b <= 1 ? "e03-prob-bar e03-prob-bar--fail" : "e03-prob-bar",
|
|
"data-count": n,
|
|
});
|
|
bar.append(svgEl("title", {}, `${fmt(a, 3)} ~ ${fmt(b, 3)} · ${n}`));
|
|
f.svg.append(bar);
|
|
});
|
|
mark(f, 1, "e03-prob-mark--one", "F = 1");
|
|
if (required) mark(f, required, "e03-prob-mark--req", `${t("E03_Required")} ${fmt(required)}`);
|
|
if (fsDet) mark(f, fsDet, "e03-prob-mark--det", t("E03_Pb_Det"));
|
|
return box("prob-hist", f.svg);
|
|
}
|
|
|
|
/** 누적분포 — 가로 F · 세로 P(F ≤ x) · F = 1 에서 Pf */
|
|
export function cdfChart(res: ProbResult, required: number | null): HTMLElement {
|
|
const c = res.stats.cdf;
|
|
if (!c) return el("div");
|
|
const xr: [number, number] = [Math.min(c.F[0], 1), Math.max(c.F[c.F.length - 1], required ?? 0)];
|
|
const f = frame("prob-cdf", xr, [0, 1], "Fs", "P(F ≤ x)");
|
|
const pts: Point[] = c.F.map((x, i) => [f.sx(x), f.sy(c.p[i])]);
|
|
f.svg.append(
|
|
svgEl("polyline", { points: pointsText(pts), class: "e03-prob-line e03-prob-line--0" }),
|
|
);
|
|
mark(f, 1, "e03-prob-mark--one", `Pf ${fmt((res.stats.pf ?? 0) * 100, 1)} %`);
|
|
if (required) mark(f, required, "e03-prob-mark--req", `${t("E03_Required")} ${fmt(required)}`);
|
|
return box("prob-cdf", f.svg);
|
|
}
|
|
|
|
function legendOf(curves: ProbCurve[]): HTMLElement {
|
|
const legend = el("div", { className: "e03-legend" });
|
|
curves.forEach((c, i) => {
|
|
const item = el("span", { className: "e03-legend__item" });
|
|
const sw = svgEl("svg", { width: 18, height: 8 });
|
|
sw.append(
|
|
svgEl("line", {
|
|
x1: 0,
|
|
x2: 18,
|
|
y1: 4,
|
|
y2: 4,
|
|
class: `e03-prob-line e03-prob-line--${i % 8}`,
|
|
}),
|
|
);
|
|
item.append(sw as unknown as HTMLElement, c.name);
|
|
legend.append(item);
|
|
});
|
|
return legend;
|
|
}
|
|
|
|
/** 민감도 — 가로 = 범위 %(0 = 아래 끝 · 100 = 위 끝) · 나머지 변수 평균 · 임계면 고정 */
|
|
export function sensitivityChart(res: ProbResult): HTMLElement {
|
|
const curves = res.sensitivity.curves;
|
|
const all = curves.flatMap((c) => c.F).filter((v): v is number => v !== null);
|
|
if (!all.length) return el("div");
|
|
const f = frame(
|
|
"prob-sens",
|
|
[0, 100],
|
|
[Math.min(...all, 1), Math.max(...all)],
|
|
t("E03_Pb_Range"),
|
|
"Fs",
|
|
);
|
|
curves.forEach((c, i) => {
|
|
const n = c.F.length;
|
|
const pts: Point[] = [];
|
|
c.F.forEach((v, k) => {
|
|
if (v !== null) pts.push([f.sx((100 * k) / Math.max(n - 1, 1)), f.sy(v)]);
|
|
});
|
|
const line = svgEl("polyline", {
|
|
points: pointsText(pts),
|
|
class: `e03-prob-line e03-prob-line--${i % 8}`,
|
|
"data-name": c.name,
|
|
});
|
|
line.append(svgEl("title", {}, `${c.name} · ${fmt(c.x[0])} ~ ${fmt(c.x[n - 1])}`));
|
|
f.svg.append(line);
|
|
});
|
|
return box("prob-sens", f.svg, legendOf(curves));
|
|
}
|
|
|
|
/** 토네이도 — 변수마다 아래 끝 · 위 끝 F 막대(평균 F 기준) · 폭 큰 순 */
|
|
export function tornadoChart(res: ProbResult): HTMLElement {
|
|
const rows = res.sensitivity.tornado;
|
|
const F0 = res.sensitivity.F_mean;
|
|
if (!rows.length || F0 === null) return el("div");
|
|
const vals = rows.flatMap((r) => [r.F_lo, r.F_hi]).filter((v): v is number => v !== null);
|
|
const pad = { ...PAD, l: 96 };
|
|
const f = frame(
|
|
"prob-tornado",
|
|
[Math.min(...vals), Math.max(...vals)],
|
|
[0, 1],
|
|
"Fs",
|
|
"",
|
|
pad,
|
|
false,
|
|
);
|
|
const step = (H - PAD.t - PAD.b) / rows.length;
|
|
rows.forEach((r, i) => {
|
|
const y = PAD.t + i * step + step * 0.15;
|
|
const hh = step * 0.7;
|
|
for (const [v, cls] of [
|
|
[r.F_lo, "e03-prob-tbar--lo"],
|
|
[r.F_hi, "e03-prob-tbar--hi"],
|
|
] as const) {
|
|
if (v === null) continue;
|
|
const [a, b] = [Math.min(f.sx(v), f.sx(F0)), Math.max(f.sx(v), f.sx(F0))];
|
|
const bar = svgEl("rect", {
|
|
x: a,
|
|
y,
|
|
width: Math.max(b - a, 0.5),
|
|
height: hh,
|
|
class: `e03-prob-tbar ${cls}`,
|
|
});
|
|
bar.append(svgEl("title", {}, `${r.name} · ${fmt(v, 3)}`));
|
|
f.svg.append(bar);
|
|
}
|
|
f.svg.append(
|
|
svgEl(
|
|
"text",
|
|
{ x: pad.l - 4, y: y + hh / 2 + 4, "text-anchor": "end", class: "e03-prob-tlabel" },
|
|
r.name,
|
|
),
|
|
);
|
|
});
|
|
mark(f, F0, "e03-prob-mark--det", fmt(F0, 3));
|
|
return box("prob-tornado", f.svg);
|
|
}
|