From 0980dd6fc58778ae1cb3ab0de2325098d138251d Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 9 Nov 2026 04:06:53 +0800 Subject: [PATCH] Make research case charts readable at narrow widths Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../smoke/research-analysis-read-smoke.mjs | 3 +- .../research-case-study.css | 28 +++++++++- .../research-case-study.tsx | 52 ++++++++++++------- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/apps/presentation/dashboard/smoke/research-analysis-read-smoke.mjs b/apps/presentation/dashboard/smoke/research-analysis-read-smoke.mjs index 4e08f6317b..43685e580f 100644 --- a/apps/presentation/dashboard/smoke/research-analysis-read-smoke.mjs +++ b/apps/presentation/dashboard/smoke/research-analysis-read-smoke.mjs @@ -23,7 +23,8 @@ const renderedStudy = renderToStaticMarkup(createElement(ResearchCaseStudy, { assert.ok(renderedStudy.includes(study.title)); assert.ok(renderedStudy.includes('Showing the saved Chinese original.')); assert.ok(renderedStudy.includes('not current results')); -assert.ok(renderedStudy.includes('unknown'), 'Missing values do not become zero'); +assert.match(renderedStudy, /候选 A<.*?>7<.*?候选 B<.*?>Not recorded<.*?候选 C<.*?>3 div { padding-left: 20px; border-left: 2px solid var(--pw-line); }.case-event-label { color: var(--pw-muted); font-size: var(--text-body); padding-top: 2px; } .research-case-study .case-timeline h4 { margin: 0 0 8px; font-size: inherit; font-weight: 600; }.case-timeline p { margin-block: 0; } .case-series { margin: 26px 0; }.case-series figcaption { font-size: var(--text-body); font-weight: 500; }.case-series figcaption span { color: var(--pw-muted); font-weight: 400; } -.case-chart-scroll { overflow-x: auto; }.case-series svg { width: 100%; min-width: 480px; fill: var(--pw-text); font: 15px system-ui; }.case-chart-axis { stroke: var(--pw-line); }.case-chart-stem { stroke: var(--pw-text); stroke-width: 2; }.case-series .case-chart-label { fill: var(--pw-muted); font-size: 12px; } -.case-series dl > div { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--pw-line); font-size: var(--text-body); }.case-series dd { margin: 0; } +.case-series-points { list-style: none; margin: 8px 0 0; padding: 0; } +.case-series-points > li { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(8ch, 1fr); + grid-template-areas: "label plot value"; + align-items: center; + gap: 8px 14px; + padding-block: 8px; + border-bottom: 1px solid var(--pw-line); + font-size: var(--text-body); + line-height: 1.5; +} +.case-chart-label { grid-area: label; } +.case-chart-value { grid-area: value; text-align: end; font-variant-numeric: tabular-nums; } +.case-chart-track { grid-area: plot; position: relative; block-size: 12px; margin-inline: 4px; } +.case-chart-track > span { position: absolute; } +.case-chart-zero { inset-block: 0; border-inline-start: 1px solid var(--pw-muted); } +.case-chart-bar { inset-block: 5px; background: var(--pw-text); } +.case-chart-mark { inset-block-start: 3px; inline-size: 6px; block-size: 6px; margin-inline-start: -3px; border-radius: 50%; background: var(--pw-text); } +@container (max-width: 520px) { + .case-series-points > li { + grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); + grid-template-areas: "label value" "plot plot"; + row-gap: 2px; + } +} .case-limitations { color: var(--pw-muted); }.case-limitations li { margin-block: 10px; }.case-read-warning { border-left: 3px solid currentColor; padding-left: 14px; } .research-case-reader :is(button,select,summary,[tabindex]):focus-visible, .case-return-reader:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 3px; } @container (max-width: 760px) { .case-reading-layout { grid-template-columns: 1fr; gap: 18px; }.case-outline { position: static; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 4px 20px; }.case-opening { margin-block: 24px; } } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.tsx b/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.tsx index 0f2826b07f..30ca9075f1 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.tsx @@ -6,29 +6,41 @@ import './research-case-study.css'; type Series = NonNullable; -/** Categorical observations, not an interpolated time series or a ranking. Gaps remain gaps. */ +/** Plot the saved observations in order on a common scale that includes zero. */ function CaseSeries({series, zh}: {series: Series; zh: boolean}) { - const values = series.points.flatMap(p => p.value === null ? [] : [p.value]); - const scale = Math.max(1, ...values.map(Math.abs)); - const low = Math.min(0, ...values.map(v => v / scale)), high = Math.max(0, ...values.map(v => v / scale)), span = high - low || 1; - const width = Math.max(700, series.points.length * 100); - const x = (i: number) => 54 + i * (width - 110) / Math.max(1, series.points.length - 1); - const y = (v: number) => 186 - (v / scale - low) * 132 / span; - const format = (v: number) => v.toLocaleString(zh ? 'zh-CN' : 'en', {maximumFractionDigits: 6}); + const values = series.points.flatMap(point => point.value === null ? [] : [point.value]); + const magnitude = Math.max(0, ...values.map(Math.abs)) || 1; + const scaled = values.map(value => value / magnitude); + const low = Math.min(0, ...scaled); + const span = Math.max(0, ...scaled) - low || 1; + const position = (value: number) => (value / magnitude - low) * 100 / span; + const zero = position(0); + const locale = zh ? 'zh-CN' : 'en'; + const decimal = new Intl.NumberFormat(locale, {maximumSignificantDigits: 17}); + const scientific = new Intl.NumberFormat(locale, {notation: 'scientific', maximumSignificantDigits: 17}); + const format = (value: number) => { + const absolute = Math.abs(value); + return (absolute !== 0 && (absolute < 1e-6 || absolute >= 1e12) ? scientific : decimal).format(value); + }; + return
{series.label} · {series.unit}
-
- `${p.label}: ${p.value === null ? (zh ? '未知' : 'unknown') : format(p.value)}`).join('; ')}> - - {series.points.map((p, i) => - {p.value !== null ? <> - {format(p.value)} - : —} - {Array.from(p.label).length > 12 ? `${Array.from(p.label).slice(0, 11).join('')}…` : p.label} - )} - -
-
{zh ? '查看数值' : 'Read values'}
{series.points.map((p, i) =>
{p.label}
{p.value === null ? (zh ? '未记录' : 'Not recorded') : `${format(p.value)} ${series.unit}`}
)}
+
    + {series.points.map((point, i) => { + const end = point.value === null ? null : position(point.value); + return
  1. + {point.label} + {point.value === null ? (zh ? '未记录' : 'Not recorded') : format(point.value)} +
  2. ; + })} +
; }