fix: 비교창 스크롤 수정 변경

main
jschoi 8 months ago
parent 20a1a4f268
commit b73d3a509d

@ -9,14 +9,14 @@ import {
} from "vue"; } from "vue";
import Plotly from "plotly.js-dist-min"; import Plotly from "plotly.js-dist-min";
/** ===== Types ===== */ /* ===== Types ===== */
type MetricKV = { key: string; value: number }; type MetricKV = { key: string; value: number };
type RunDetailType = { type RunDetailType = {
info: any; info: any;
data: { metrics: MetricKV[]; params?: any[]; tags?: any[] }; data: { metrics: MetricKV[]; params?: any[]; tags?: any[] };
}; };
/** ===== Props / Emits ===== */ /* ===== Props / Emits ===== */
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue: boolean; modelValue: boolean;
@ -46,7 +46,7 @@ const emit = defineEmits<{
(e: "update:selectedMetricKeys", v: string[]): void; (e: "update:selectedMetricKeys", v: string[]): void;
}>(); }>();
/** ===== local refs ===== */ /* ===== local refs ===== */
const dialogOpen = computed({ const dialogOpen = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (v: boolean) => emit("update:modelValue", v), set: (v: boolean) => emit("update:modelValue", v),
@ -63,7 +63,7 @@ const selectedMetricKeysProxy = computed({
const elCompare = ref<HTMLDivElement | null>(null); const elCompare = ref<HTMLDivElement | null>(null);
const loading = ref(false); const loading = ref(false);
/** ===== helpers ===== */ /* ===== helpers ===== */
const fmtNumber = (v: number | null, digits = 3) => { const fmtNumber = (v: number | null, digits = 3) => {
if (v == null || !Number.isFinite(v)) return ""; if (v == null || !Number.isFinite(v)) return "";
const abs = Math.abs(v); const abs = Math.abs(v);
@ -75,8 +75,8 @@ const fmtNumber = (v: number | null, digits = 3) => {
const normalizeArray = (vals: (number | null)[]) => { const normalizeArray = (vals: (number | null)[]) => {
const xs = vals.filter((v): v is number => Number.isFinite(v as number)); const xs = vals.filter((v): v is number => Number.isFinite(v as number));
if (xs.length === 0) return vals; if (xs.length === 0) return vals;
const min = Math.min(...xs), const min = Math.min(...xs);
max = Math.max(...xs); const max = Math.max(...xs);
if (max === min) return vals.map((v) => (v == null ? v : 1)); if (max === min) return vals.map((v) => (v == null ? v : 1));
return vals.map((v) => (v == null ? v : (v - min) / (max - min))); return vals.map((v) => (v == null ? v : (v - min) / (max - min)));
}; };
@ -86,7 +86,7 @@ const valueOf = (run: RunDetailType, key: string): number | null => {
return Number.isFinite(m as number) ? Number(m) : null; return Number.isFinite(m as number) ? Number(m) : null;
}; };
/** ===== derived ===== */ /* ===== derived ===== */
const compareRuns = computed<RunDetailType[]>( const compareRuns = computed<RunDetailType[]>(
() => () =>
selectedRunIdsProxy.value selectedRunIdsProxy.value
@ -109,7 +109,25 @@ const activeMetricKeys = computed<string[]>(() =>
: commonMetricKeys.value, : commonMetricKeys.value,
); );
/** ===== chart ===== */ /* ===== responsive widths for horizontal scroll =====
- 그래프: 막대 개수에 비례해 내부 너비를 크게 잡아 가로 스크롤 허용
- 테이블: (메트릭 + Run 컬럼 1) * 160px 정도로 최소 너비 설정
*/
const chartInnerWidth = computed(() => {
// byMetric: X = metric , byRun: X = run
const xCount =
props.compareChartMode === "byMetric"
? activeMetricKeys.value.length
: compareRuns.value.length;
// 140px, 900px
return Math.max(900, xCount * 140 + 240);
});
const tableInnerWidth = computed(() => {
const cols = 1 + activeMetricKeys.value.length; // Run + metric
return Math.max(900, cols * 160);
});
/* ===== chart ===== */
const commonLayout: Partial<any> = { const commonLayout: Partial<any> = {
autosize: true, autosize: true,
margin: { t: 28, r: 12, b: 56, l: 56 }, margin: { t: 28, r: 12, b: 56, l: 56 },
@ -181,6 +199,7 @@ function drawCompareChart() {
? createTracesByMetric(metricKeys, runsData) ? createTracesByMetric(metricKeys, runsData)
: createTracesByRun(metricKeys, runsData); : createTracesByRun(metricKeys, runsData);
// byMetric ()
if (props.compareChartMode === "byMetric") { if (props.compareChartMode === "byMetric") {
const varianceOrder = metricKeys const varianceOrder = metricKeys
.map((k, idx) => { .map((k, idx) => {
@ -216,7 +235,7 @@ function drawCompareChart() {
); );
} }
/** ===== load & watch ===== */ /* ===== load & watch ===== */
async function loadCompareData() { async function loadCompareData() {
if (!dialogOpen.value) return; if (!dialogOpen.value) return;
loading.value = true; loading.value = true;
@ -250,9 +269,9 @@ onBeforeUnmount(() => window.removeEventListener("resize", onResize));
</script> </script>
<template> <template>
<v-dialog v-model="dialogOpen" max-width="1000"> <!-- 가로도 넉넉히, 세로는 90vh 제한 -->
<v-dialog v-model="dialogOpen" max-width="80vw">
<v-card class="rounded-lg overflow-hidden"> <v-card class="rounded-lg overflow-hidden">
<!-- 헤더: Deploy Model 스타일 적용 -->
<v-card-title <v-card-title
class="text-white font-weight-bold text-h6" class="text-white font-weight-bold text-h6"
style="background-color: #1976d2" style="background-color: #1976d2"
@ -260,128 +279,154 @@ onBeforeUnmount(() => window.removeEventListener("resize", onResize));
Compare Runs Compare Runs
</v-card-title> </v-card-title>
<!-- 상단: 선택 섹션 --> <!-- 본문 전체에 세로 스크롤 -->
<v-card-text class="pa-6"> <div class="scroll-y">
<v-row dense class="mb-2"> <!-- 상단 선택 섹션 -->
<v-col cols="12"> <v-card-text class="pa-6">
<v-subheader class="font-weight-medium mb-2" <v-row dense class="mb-2">
>Select Runs</v-subheader <v-col cols="12">
> <v-subheader class="font-weight-medium mb-2"
<v-autocomplete >Select Runs</v-subheader
v-model="selectedRunIdsProxy" >
:items="items" <v-autocomplete
item-title="label" v-model="selectedRunIdsProxy"
item-value="value" :items="items"
chips item-title="label"
multiple item-value="value"
closable-chips chips
variant="outlined" multiple
density="comfortable" closable-chips
hide-details variant="outlined"
:disabled="loading" density="comfortable"
placeholder="Select runs to compare" hide-details
/> :disabled="loading"
</v-col> placeholder="Select runs to compare"
</v-row> />
</v-col>
</v-row>
<v-expand-transition> <v-expand-transition>
<v-alert <v-alert
v-if="!loading && selectedRunIdsProxy.length < 2" v-if="!loading && selectedRunIdsProxy.length < 2"
type="info" type="info"
variant="tonal" variant="tonal"
density="compact" density="compact"
class="mt-2" class="mt-2"
> >
2 이상의 Run을 선택해 주세요. 2 이상의 Run을 선택해 주세요.
</v-alert> </v-alert>
</v-expand-transition> </v-expand-transition>
<v-row dense class="mt-4"> <v-row dense class="mt-4">
<v-col cols="12"> <v-col cols="12">
<v-subheader class="font-weight-medium mb-2">Metrics</v-subheader> <v-subheader class="font-weight-medium mb-2">Metrics</v-subheader>
<v-autocomplete <v-autocomplete
v-model="selectedMetricKeysProxy" v-model="selectedMetricKeysProxy"
:items="activeMetricKeys.length ? activeMetricKeys : []" :items="activeMetricKeys.length ? activeMetricKeys : []"
:menu-props="{ maxHeight: 360 }" :menu-props="{ maxHeight: 360 }"
chips chips
multiple multiple
closable-chips closable-chips
variant="outlined" variant="outlined"
density="comfortable" density="comfortable"
hide-details hide-details
:disabled="loading || compareRuns.length === 0" :disabled="loading || compareRuns.length === 0"
placeholder="Select metrics (empty = all common)" placeholder="Select metrics (empty = all common)"
/> />
<div class="d-flex align-center mt-2 ga-2"> <div class="d-flex align-center mt-2 ga-2">
<v-chip size="small" color="primary" variant="tonal"> <v-chip size="small" color="primary" variant="tonal">
Mode: Mode:
{{ {{
props.compareChartMode === "byMetric" ? "By Metric" : "By Run" props.compareChartMode === "byMetric"
}} ? "By Metric"
</v-chip> : "By Run"
<v-chip size="small" color="secondary" variant="tonal"> }}
{{ props.normalizeValues ? "Normalized" : "Raw values" }} </v-chip>
</v-chip> <v-chip size="small" color="secondary" variant="tonal">
<v-chip {{ props.normalizeValues ? "Normalized" : "Raw values" }}
v-if="props.baselineRunId" </v-chip>
size="small" <v-chip
color="info" v-if="props.baselineRunId"
variant="tonal" size="small"
> color="info"
Baseline: {{ props.baselineRunId }} variant="tonal"
</v-chip> >
</div> Baseline: {{ props.baselineRunId }}
</v-col> </v-chip>
</v-row> </div>
</v-card-text> </v-col>
</v-row>
</v-card-text>
<!-- 차트 & 테이블 섹션 ( 카드로 감싸기) --> <!-- 차트 (가로 스크롤) -->
<v-card-text class="pt-0 px-6 pb-2"> <v-card-text class="pt-0 px-6 pb-2">
<v-card variant="tonal" class="mb-4"> <v-card variant="tonal" class="mb-4">
<v-card-title class="py-2 px-4">Metrics (grouped bar)</v-card-title> <v-card-title class="py-2 px-4">Metrics (grouped bar)</v-card-title>
<v-divider /> <v-divider />
<v-card-text class="px-4"> <v-card-text class="px-4">
<div v-if="loading" class="my-2"> <div v-if="loading" class="my-2">
<v-progress-linear indeterminate /> <v-progress-linear indeterminate />
</div> </div>
<div ref="elCompare" style="width: 100%; height: 420px"></div> <div class="scroll-x">
</v-card-text> <div
</v-card> ref="elCompare"
:style="{ width: chartInnerWidth + 'px', height: '420px' }"
/>
</div>
</v-card-text>
</v-card>
<v-card variant="tonal"> <!-- 테이블 (가로 스크롤) -->
<v-card-title class="py-2 px-4">Comparison Table</v-card-title> <v-card variant="tonal">
<v-divider /> <v-card-title class="py-2 px-4">Comparison Table</v-card-title>
<v-card-text class="px-0"> <v-divider />
<v-table density="comfortable"> <v-card-text class="px-0">
<thead> <div class="scroll-x">
<tr> <div :style="{ minWidth: tableInnerWidth + 'px' }">
<th style="width: 28%">Run</th> <v-table density="comfortable">
<th v-for="k in activeMetricKeys" :key="k">{{ k }}</th> <thead>
</tr> <tr>
</thead> <th style="width: 28%">Run</th>
<tbody> <th v-for="k in activeMetricKeys" :key="k">{{ k }}</th>
<tr v-for="r in compareRuns" :key="r.info.run_id"> </tr>
<td class="text-no-wrap"> </thead>
{{ r.info.run_name || r.info.run_id }} <tbody>
</td> <tr v-for="r in compareRuns" :key="r.info.run_id">
<td v-for="k in activeMetricKeys" :key="k"> <td class="text-no-wrap">
{{ r.data.metrics.find((m) => m.key === k)?.value ?? "—" }} {{ r.info.run_name || r.info.run_id }}
</td> </td>
</tr> <td v-for="k in activeMetricKeys" :key="k">
</tbody> {{
</v-table> r.data.metrics.find((m) => m.key === k)?.value ??
</v-card-text> "—"
</v-card> }}
</v-card-text> </td>
</tr>
</tbody>
</v-table>
</div>
</div>
</v-card-text>
</v-card>
</v-card-text>
</div>
<!-- 액션 --> <!-- 액션 -->
<v-card-actions class="justify-end"> <v-card-actions class="justify-end">
<v-btn variant="text" class="text-white" @click="dialogOpen = false"> <v-btn variant="text" class="text-white" @click="dialogOpen = false"
CLOSE >CLOSE</v-btn
</v-btn> >
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
</template> </template>
<style scoped></style> <style scoped>
.scroll-y {
max-height: 90vh; /* 다이얼로그 본문 세로 스크롤 */
overflow-y: auto;
}
.scroll-x {
overflow-x: auto; /* 가로 스크롤 */
}
</style>

Loading…
Cancel
Save