fix(charts): fit analysis windows across viewport sizes
This commit is contained in:
@@ -172,10 +172,10 @@ export const InteractiveChart = memo(function InteractiveChartView(props: Answer
|
||||
</dl>
|
||||
</motion.section> : null}
|
||||
<p className="flex items-start gap-1.5 border-t pt-2.5 text-[11px] leading-5 text-muted-foreground">
|
||||
<Database size={12} className="mt-1 shrink-0" /><span>{fullRange ? labels.length : `${labels.length} / ${props.x_data.length}`} 个数据点 · {visible.length} 组序列<br />来源:{props.source}</span>
|
||||
<Database size={12} className="mt-1 shrink-0" /><span className="min-w-0 break-words [overflow-wrap:anywhere]">{fullRange ? labels.length : `${labels.length} / ${props.x_data.length}`} 个数据点 · {visible.length} 组序列<br />来源:{props.source}</span>
|
||||
</p>
|
||||
</>;
|
||||
return <Collapsible asChild><Card className={`answer-ui-chart overflow-hidden ${props.embedded || props.expanded ? "border-0 shadow-none" : ""} ${props.expanded ? "@container flex h-full min-h-[360px] min-w-0 flex-1 flex-col" : ""}`} role={props.embedded ? undefined : "group"} aria-label={props.embedded ? undefined : props.title}
|
||||
return <Collapsible asChild><Card className={`answer-ui-chart overflow-hidden ${props.embedded || props.expanded ? "border-0 shadow-none" : ""} ${props.expanded ? "@container flex h-full min-h-0 min-w-0 flex-1 flex-col" : ""}`} role={props.embedded ? undefined : "group"} aria-label={props.embedded ? undefined : props.title}
|
||||
onPointerDownCapture={() => { pointer.current = true; }}
|
||||
onKeyDownCapture={() => {
|
||||
pointer.current = false;
|
||||
@@ -200,8 +200,8 @@ export const InteractiveChart = memo(function InteractiveChartView(props: Answer
|
||||
</CardHeader>
|
||||
<CardContent className={`${props.expanded ? "flex min-h-0 flex-1 flex-col" : "space-y-3"} px-3.5 pb-3.5 pt-0`}>
|
||||
{migrationNotice ? <p role="status" className="text-xs leading-5 text-muted-foreground">{migrationNotice}</p> : null}
|
||||
<Tabs value={view} onValueChange={setView} className={props.expanded ? "grid min-h-0 flex-1 grid-cols-1 grid-rows-[auto_auto_minmax(220px,1fr)_minmax(0,auto)] gap-x-4 gap-y-2 @min-[640px]:grid-cols-[minmax(0,1fr)_208px] @min-[640px]:grid-rows-[auto_auto_minmax(220px,1fr)]" : undefined}>
|
||||
<TabsList aria-label={`${props.title}显示方式`} className={`${props.expanded ? "col-start-1 row-start-1" : "mb-3"} relative grid w-full grid-cols-3 [&>[role=tab]]:relative [&>[role=tab]]:z-10 [&>[role=tab][data-state=active]]:bg-transparent [&>[role=tab][data-state=active]]:shadow-none`}>
|
||||
<Tabs value={view} onValueChange={setView} className={props.expanded ? "grid min-h-0 flex-1 grid-cols-1 grid-rows-[auto_auto_minmax(120px,1fr)_minmax(0,auto)] gap-x-3 gap-y-2 @min-[560px]:grid-cols-[minmax(0,1fr)_176px] @min-[560px]:grid-rows-[auto_auto_minmax(120px,1fr)]" : undefined}>
|
||||
<TabsList aria-label={`${props.title}显示方式`} className={`${props.expanded ? "col-start-1 row-start-1 min-w-0" : "mb-3"} relative grid w-full grid-cols-3 [&>[role=tab]]:relative [&>[role=tab]]:z-10 [&>[role=tab][data-state=active]]:bg-transparent [&>[role=tab][data-state=active]]:shadow-none`}>
|
||||
<motion.span aria-hidden className="pointer-events-none absolute inset-y-1 left-1 rounded-md bg-card shadow-xs"
|
||||
style={{ width: "calc((100% - 0.5rem) / 3)" }}
|
||||
initial={false} animate={{ x: view === "line" ? "0%" : view === "bar" ? "100%" : "200%" }}
|
||||
@@ -210,10 +210,10 @@ export const InteractiveChart = memo(function InteractiveChartView(props: Answer
|
||||
<TabsTrigger value="bar"><BarChart3 size={13} />柱状</TabsTrigger>
|
||||
<TabsTrigger value="table"><Table2 size={13} />数据</TabsTrigger>
|
||||
</TabsList>
|
||||
<aside className={props.expanded ? "col-start-1 row-start-4 max-h-44 min-h-0 space-y-3 overflow-y-auto border-t border-border pt-3 @min-[640px]:col-start-2 @min-[640px]:row-start-1 @min-[640px]:row-span-3 @min-[640px]:max-h-none @min-[640px]:border-t-0 @min-[640px]:border-l @min-[640px]:pt-0 @min-[640px]:pl-3" : undefined}>
|
||||
<div className={`answer-ui-chart-series mb-2 flex flex-wrap gap-2 ${props.expanded ? "@min-[640px]:flex-col @min-[640px]:flex-nowrap @min-[640px]:items-stretch" : ""}`} aria-label="显示的数据序列">
|
||||
<aside className={props.expanded ? "col-start-1 row-start-4 max-h-32 min-h-0 min-w-0 space-y-2 overflow-y-auto border-t border-border pt-2 @min-[560px]:col-start-2 @min-[560px]:row-start-1 @min-[560px]:row-span-3 @min-[560px]:max-h-none @min-[560px]:border-t-0 @min-[560px]:border-l @min-[560px]:pt-0 @min-[560px]:pl-3" : undefined}>
|
||||
<div className={`answer-ui-chart-series mb-2 flex flex-wrap gap-2 ${props.expanded ? "@min-[560px]:flex-col @min-[560px]:flex-nowrap @min-[560px]:items-stretch" : ""}`} aria-label="显示的数据序列">
|
||||
{props.series.map((series, index) => <Button key={index} size="sm" variant="outline"
|
||||
className={`h-auto min-h-8 max-w-full gap-1.5 whitespace-normal text-left text-[11px] ${props.expanded ? "justify-start" : ""}`} aria-pressed={!hidden.includes(index)}
|
||||
className={`h-auto min-h-8 min-w-0 max-w-full gap-1.5 whitespace-normal text-left text-[11px] after:inset-0 ${props.expanded ? "justify-start" : ""}`} aria-pressed={!hidden.includes(index)}
|
||||
disabled={visible.length === 1 && !hidden.includes(index)}
|
||||
onClick={() => { setAnimateInteraction(pointer.current && !reducedMotion); patch({ hidden: hidden.includes(index) ? hidden.filter((item) => item !== index) : [...hidden, index],
|
||||
table: { query: "", ...table, sort: null, selection: [] } }, ["hidden", "table/sort"]); }}>
|
||||
@@ -230,7 +230,7 @@ export const InteractiveChart = memo(function InteractiveChartView(props: Answer
|
||||
</div> : null}
|
||||
{props.expanded ? metadata : null}
|
||||
</aside>
|
||||
<div className={props.expanded ? "col-start-1 row-start-2" : undefined}>
|
||||
<div className={props.expanded ? "col-start-1 row-start-2 min-w-0" : undefined}>
|
||||
{!fullRange ? <p className="mb-2 text-[11px] text-muted-foreground">{props.x_data[range[0]]} 至 {props.x_data[range[1]]}</p> : null}
|
||||
<CollapsibleContent className="mb-2 space-y-2 rounded-lg border bg-muted/30 p-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -244,10 +244,10 @@ export const InteractiveChart = memo(function InteractiveChartView(props: Answer
|
||||
onClick={() => setRange([0, props.x_data.length - 1])}>重置范围</Button>
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
<motion.div className={props.expanded ? "col-start-1 row-start-3 min-h-0" : undefined} data-chart-view={view} initial={false} animate={contentMotion}>
|
||||
<motion.div className={props.expanded ? "col-start-1 row-start-3 min-h-0 min-w-0" : undefined} data-chart-view={view} initial={false} animate={contentMotion}>
|
||||
{(["line", "bar"] as const).map((type) => <TabsContent key={type} value={type} className={props.expanded ? "h-full min-h-0" : undefined}>
|
||||
<div ref={view === type ? plot : undefined} role="group" aria-label={`${props.title},按回车查看数据点,方向键切换数据点`} tabIndex={0}
|
||||
className={`${props.expanded ? "h-full min-h-0 overflow-auto" : ""} rounded-md focus-visible:outline-2 focus-visible:outline-ring`}
|
||||
className={`${props.expanded ? (type === "bar" && horizontalBars ? "h-full min-h-0 overflow-x-hidden overflow-y-auto" : "h-full min-h-0 overflow-hidden") : ""} rounded-md focus-visible:outline-2 focus-visible:outline-ring`}
|
||||
onKeyDown={(event) => {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
if (["Enter", "ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) {
|
||||
|
||||
@@ -312,9 +312,9 @@ export function InvestigationPresentationPanel({
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<FloatingWindow title={item.chart.title} fillContent
|
||||
openRequestKey={chartWindowKey === undefined ? undefined : JSON.stringify([chartWindowKey, item])}>
|
||||
{() => <div className="flex h-full min-h-[420px] min-w-0 flex-col gap-3">
|
||||
{() => <div className="flex h-full min-h-0 min-w-0 flex-col gap-2">
|
||||
<AgentChartView chart={item.chart} embedded expanded />
|
||||
<div className="shrink-0">{references(item.evidence_refs)}</div>
|
||||
{item.evidence_refs.length ? <div className="max-h-[35%] shrink-0 overflow-auto">{references(item.evidence_refs)}</div> : null}
|
||||
</div>}
|
||||
</FloatingWindow>
|
||||
<Button
|
||||
|
||||
@@ -116,3 +116,60 @@ test("structured analysis charts open responsive windows in local preview and hi
|
||||
await expect(popup).toBeVisible();
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
|
||||
test("live charts fit desktop and narrow windows without outer scrollbars", async ({ page }, testInfo) => {
|
||||
const task = investigationFixture(1, "实时图表紧凑布局核查");
|
||||
const mock = await mockInvestigationPresentation(page, [task]);
|
||||
await selectInvestigation(page, task.title);
|
||||
await mock.emit(task, chart(1));
|
||||
const popup = page.getByRole("dialog", { name: "合成压力分析 1", exact: true });
|
||||
const content = popup.locator("[data-floating-window-content]");
|
||||
await expect(popup.getByRole("img", { name: "合成压力分析 1", exact: true })).toBeVisible();
|
||||
for (const [width, height] of [[1440, 900], [1280, 720], [800, 600], [375, 540], [320, 540]]) {
|
||||
await page.setViewportSize({ width, height });
|
||||
await expect.poll(() => content.evaluate((node) => ({
|
||||
x: node.scrollWidth - node.clientWidth,
|
||||
y: node.scrollHeight - node.clientHeight
|
||||
}))).toEqual({ x: 0, y: 0 });
|
||||
const plot = popup.getByRole("img", { name: "合成压力分析 1", exact: true });
|
||||
expect((await plot.boundingBox())!.height).toBeGreaterThanOrEqual(120);
|
||||
await testInfo.attach(`live-chart-fit-${width}`, {
|
||||
body: await popup.screenshot({ path: testInfo.outputPath(`live-chart-fit-${width}.png`) }), contentType: "image/png"
|
||||
});
|
||||
}
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await popup.getByRole("button", { name: "筛选数据范围", exact: true }).click();
|
||||
await expect(popup.getByRole("combobox", { name: "区间起点" })).toBeVisible();
|
||||
await expect.poll(() => content.evaluate((node) => node.scrollHeight - node.clientHeight)).toBe(0);
|
||||
await popup.getByRole("tab", { name: "数据", exact: true }).click();
|
||||
await expect(popup.getByRole("table")).toBeVisible();
|
||||
expect(mock.mutations).toEqual([]);
|
||||
});
|
||||
|
||||
test("SCADA chart axes and legends fit every nested container", async ({ page }, testInfo) => {
|
||||
const task = investigationFixture(1, "SCADA 压力布局核查");
|
||||
const mock = await mockInvestigationPresentation(page, [task]);
|
||||
await selectInvestigation(page, task.title);
|
||||
await mock.emit(task, { action: "open_chart", chart_id: "scada-pressure", evidence_refs: ["synthetic:pressure"], chart: {
|
||||
title: "SCADA 回放压力:异常前后(2026-07-16,UTC+8)", chart_type: "line",
|
||||
x_axis_name: "时间(UTC+8)", y_axis_name: "压力(m)", x_data: ["07:30", "07:45", "08:00"],
|
||||
series: [{ name: "测点 123748(实测回放)", data: [26.2, 25.7, 23.5] },
|
||||
{ name: "测点 159673(实测回放)", data: [24.9, 24.87, 21.6] }]
|
||||
} });
|
||||
const popup = page.getByRole("dialog", { name: "SCADA 回放压力:异常前后(2026-07-16,UTC+8)", exact: true });
|
||||
await expect(popup).toBeVisible();
|
||||
await expect.poll(() => popup.evaluate((node) => Array.from(node.querySelectorAll<HTMLElement>("*"))
|
||||
.filter((element) => {
|
||||
const style = getComputedStyle(element);
|
||||
return (style.overflowX === "auto" || style.overflowX === "scroll" || style.overflowY === "auto" || style.overflowY === "scroll") &&
|
||||
(element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight);
|
||||
}).map((element) => ({ class: element.className, x: element.scrollWidth - element.clientWidth, y: element.scrollHeight - element.clientHeight }))
|
||||
)).toEqual([]);
|
||||
const plot = popup.getByRole("group", { name: /按回车查看数据点/ });
|
||||
await expect.poll(() => plot.evaluate((element) => ({
|
||||
x: element.scrollWidth - element.clientWidth, y: element.scrollHeight - element.clientHeight
|
||||
}))).toEqual({ x: 0, y: 0 });
|
||||
await testInfo.attach("scada-chart-layout", {
|
||||
body: await popup.screenshot({ path: testInfo.outputPath("scada-chart-layout.png") }), contentType: "image/png"
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user