Skip to content

Commit 50cec0a

Browse files
authored
Add TimeSeriesComparisonTable (#7184)
# Overview Add ComparisonView with table # Details Provide a Comparison Table Section with slider and render list of comparion table based on group logics. Currently we use it in fanout data render section. fanout: fetch all data in one api, and fanout the api reponse to each ui component (this is esp used for big data component such as compiler) Slider: User can slide from both side to refresh the data in all tables for comparison # Demo Link: https://torchci-git-add-timeseriescomparetable-fbopensource.vercel.app/benchmark/compilers_regression ## Chart and table ![chart table](https://github.com/user-attachments/assets/a177684f-fdc6-493c-b0db-ca7ad795dfc9) ## Chart Select comits ![cahrtSelectCommit](https://github.com/user-attachments/assets/3b12036c-2c5b-4e48-98c6-a3764ca5d6de) ## Scroll left and right with the table slider ![tableSlider](https://github.com/user-attachments/assets/43190c6c-441b-4be9-817d-91bec6390f5a) # Next step: - Add hyperlink - make the chart interactive when click confirm - make the Commits on side table interative to filter the data based on commits.
1 parent c6c5359 commit 50cec0a

File tree

18 files changed

+1095
-67
lines changed

18 files changed

+1095
-67
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Box } from "@mui/system";
2+
import { useEffect, useRef, useState } from "react";
3+
4+
export type StickyBarProps = {
5+
children: React.ReactNode;
6+
height?: number;
7+
offset: number;
8+
zIndex?: number;
9+
onMount?: (h: number) => void;
10+
onUnmount?: (h: number) => void;
11+
/** Horizontal alignment of content inside the bar */
12+
align?: "left" | "center" | "right";
13+
/** Should children keep their natural width ("fit") or stretch ("full") */
14+
contentMode?: "fit" | "full";
15+
};
16+
17+
export const StickyBar: React.FC<StickyBarProps> = ({
18+
children,
19+
height = 48,
20+
offset,
21+
zIndex = 900,
22+
onMount,
23+
onUnmount,
24+
align = "left",
25+
contentMode = "fit",
26+
}) => {
27+
const ref = useRef<HTMLDivElement>(null);
28+
const [isSticky, setIsSticky] = useState(false);
29+
30+
// Let parent know about mount/unmount (for stacking offset logic)
31+
useEffect(() => {
32+
onMount?.(height);
33+
return () => onUnmount?.(height);
34+
}, [height, onMount, onUnmount]);
35+
36+
useEffect(() => {
37+
const observer = new IntersectionObserver(
38+
([entry]) => {
39+
// < 0.99
40+
setIsSticky(entry.intersectionRatio < 0.99);
41+
},
42+
{
43+
threshold: Array.from({ length: 101 }, (_, i) => i / 100),
44+
// 0,0.01,0.02,...,1,
45+
}
46+
);
47+
48+
if (ref.current) observer.observe(ref.current);
49+
return () => observer.disconnect();
50+
}, []);
51+
52+
const justify =
53+
align === "center"
54+
? "center"
55+
: align === "right"
56+
? "flex-end"
57+
: "flex-start";
58+
59+
return (
60+
<>
61+
{/* Sentinel keeps layout height stable */}
62+
<div ref={ref} style={{ height }} />
63+
{/* Outer bar: full width, sticky */}
64+
<Box
65+
sx={{
66+
position: isSticky ? "sticky" : "static",
67+
top: offset,
68+
zIndex,
69+
borderColor: "divider",
70+
height,
71+
width: 1, // full width of parent
72+
display: "flex",
73+
alignItems: "center",
74+
justifyContent: justify,
75+
px: 2,
76+
boxSizing: "border-box",
77+
}}
78+
>
79+
{/* Inner container: controls how children size themselves */}
80+
<Box
81+
sx={{
82+
width: contentMode === "fit" ? "fit-content" : "100%",
83+
maxWidth: "100%",
84+
display: "inline-flex",
85+
alignItems: "center",
86+
gap: 1,
87+
"& > *": { flex: "0 0 auto", minWidth: "auto" }, // don’t stretch children
88+
}}
89+
>
90+
{children}
91+
</Box>
92+
</Box>
93+
<div ref={ref} style={{ height }} />
94+
</>
95+
);
96+
};
Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Paper, Typography } from "@mui/material";
22
import { Box } from "@mui/system";
33
import { useMemo } from "react";
4+
import BenchmarkTimeSeriesChartGroup from "./components/BenchmarkTimeSeriesChartGroup";
45
import {
56
BenchmarkChartSectionConfig,
67
BenchmarkTimeSeriesInput,
78
makeGroupKeyAndLabel,
89
passesFilter,
9-
} from "../helper";
10-
import BenchmarkTimeSeriesChartGroup from "./BenchmarkTimeSeriesChartGroup";
10+
} from "./helper";
1111

1212
const styles = {
1313
container: {
@@ -52,7 +52,9 @@ export default function BenchmarkChartSection({
5252
gi,
5353
chartSectionConfig.groupByFields
5454
);
55-
if (!m.has(key)) m.set(key, { key, labels, items: [] });
55+
if (!m.has(key)) {
56+
m.set(key, { key, labels, items: [] });
57+
}
5658
m.get(key)!.items.push(s);
5759
}
5860
return m;
@@ -63,34 +65,37 @@ export default function BenchmarkChartSection({
6365
}
6466

6567
return (
66-
<Box sx={styles.container}>
67-
{Array.from(groupMap.entries()).map(([key, data]) => {
68-
if (!data) return null;
69-
const op = chartSectionConfig.chartGroup?.renderOptions;
70-
const title = data.labels.join(" ");
68+
<Box sx={{ m: 1 }}>
69+
<Typography variant="h5"> Time Series Chart Section </Typography>
70+
<Box sx={styles.container}>
71+
{Array.from(groupMap.entries()).map(([key, data]) => {
72+
if (!data) return null;
73+
const op = chartSectionConfig.chartGroup?.renderOptions;
74+
const title = data.labels.join(" ");
7175

72-
let renderOptions = chartSectionConfig.chartGroup?.renderOptions;
73-
if (op && op.pass_section_title) {
74-
renderOptions = {
75-
...renderOptions,
76-
titleSuffix: `/${title}`,
77-
};
78-
}
79-
return (
80-
<Box key={key} sx={styles.groupBox}>
81-
<Paper sx={styles.paper}>
82-
<Typography variant="h4">{title.toUpperCase()}</Typography>
83-
<BenchmarkTimeSeriesChartGroup
84-
data={data.items}
85-
chartGroup={chartSectionConfig.chartGroup}
86-
onConfirm={(payload: any) => {
87-
onChange?.(payload);
88-
}}
89-
/>
90-
</Paper>
91-
</Box>
92-
);
93-
})}
76+
let renderOptions = chartSectionConfig.chartGroup?.renderOptions;
77+
if (op && op.pass_section_title) {
78+
renderOptions = {
79+
...renderOptions,
80+
titleSuffix: `/${title}`,
81+
};
82+
}
83+
return (
84+
<Box key={key} sx={styles.groupBox}>
85+
<Paper sx={styles.paper}>
86+
<Typography variant="h6">{title.toUpperCase()}</Typography>
87+
<BenchmarkTimeSeriesChartGroup
88+
data={data.items}
89+
chartGroup={chartSectionConfig.chartGroup}
90+
onConfirm={(payload: any) => {
91+
onChange?.(payload);
92+
}}
93+
/>
94+
</Paper>
95+
</Box>
96+
);
97+
})}
98+
</Box>
9499
</Box>
95100
);
96101
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { Typography } from "@mui/material";
2+
import { DataGrid, GridColDef, GridRowModel } from "@mui/x-data-grid";
3+
import { useMemo } from "react";
4+
import { ComparisonTableConfig } from "../../../helper";
5+
import { getComparisionTableConlumnRendering } from "./ComparisonTableColumnRendering";
6+
import { SnapshotRow, ToComparisonTableRow } from "./ComparisonTableHelpers";
7+
8+
export function ComparisonTable({
9+
data,
10+
lWorkflowId,
11+
rWorkflowId,
12+
config,
13+
columnOrder,
14+
title = "Group",
15+
}: {
16+
data: SnapshotRow[];
17+
lWorkflowId: string | null;
18+
rWorkflowId: string | null;
19+
config: ComparisonTableConfig;
20+
columnOrder?: string[]; // optional preferred ordering of columns
21+
title?: string;
22+
}) {
23+
// group raw data into rows, each row contains all values across workflowIds
24+
const rows: GridRowModel[] = useMemo(() => {
25+
return ToComparisonTableRow(config, data);
26+
}, [data]);
27+
28+
// iterate the column map in row data, and get all column names
29+
const allColumns = useMemo(() => {
30+
const s = new Set<string>();
31+
rows.forEach((r) =>
32+
Object.values(r.byWorkflow).forEach((cols) => {
33+
Object.keys(cols ?? {}).forEach((k) => s.add(k));
34+
})
35+
);
36+
const auto = Array.from(s).sort();
37+
if (!columnOrder || columnOrder.length === 0) return auto;
38+
const head = columnOrder.filter((c) => s.has(c));
39+
const tail = auto.filter((c) => !head.includes(c));
40+
return [...head, ...tail];
41+
}, [rows, columnOrder]);
42+
43+
// Form the columns
44+
const columns: GridColDef[] = useMemo(
45+
() =>
46+
getComparisionTableConlumnRendering(
47+
allColumns,
48+
lWorkflowId,
49+
rWorkflowId,
50+
config
51+
),
52+
[allColumns, lWorkflowId, rWorkflowId, title]
53+
);
54+
55+
return (
56+
<>
57+
<Typography variant="h6">{title.toUpperCase()}</Typography>
58+
<Typography variant="body2">
59+
{lWorkflowId} - {rWorkflowId}
60+
</Typography>
61+
<DataGrid
62+
density="compact"
63+
disableRowSelectionOnClick
64+
rows={rows}
65+
columns={columns}
66+
getRowId={(r) => r.id}
67+
sx={{
68+
"& .MuiDataGrid-cell": {
69+
py: 0, // less vertical padding
70+
fontSize: "0.75rem",
71+
},
72+
"& .MuiDataGrid-columnHeaders": {
73+
minHeight: 32,
74+
maxHeight: 32,
75+
lineHeight: "32px",
76+
fontSize: "0.75rem",
77+
},
78+
"& .MuiDataGrid-row": {
79+
minHeight: 32,
80+
maxHeight: 32,
81+
},
82+
}}
83+
hideFooter
84+
/>
85+
</>
86+
);
87+
}

0 commit comments

Comments
 (0)