Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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</,
'Saved chart observations stay in author order, with numbers and the unrecorded value visible together');
for (const section of study.sections) assert.ok(renderedStudy.includes(section.title));
const goals = [
{goalId:'parent',title:'Benchmark'},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,32 @@
.case-timeline li > 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; } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,41 @@ import './research-case-study.css';

type Series = NonNullable<TrajectoryCaseStudy['sections'][number]['series']>;

/** 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 <figure className="case-series">
<figcaption>{series.label} <span>· {series.unit}</span></figcaption>
<div className="case-chart-scroll" tabIndex={0} role="region" aria-label={series.label}>
<svg viewBox={`0 0 ${width} 238`} style={{minWidth: width}} role="img" aria-label={series.points.map(p => `${p.label}: ${p.value === null ? (zh ? '未知' : 'unknown') : format(p.value)}`).join('; ')}>
<line x1="30" x2={width - 25} y1={y(0)} y2={y(0)} className="case-chart-axis"/>
{series.points.map((p, i) => <g key={i}>
{p.value !== null ? <><line x1={x(i)} x2={x(i)} y1={y(0)} y2={y(p.value)} className="case-chart-stem"/>
<circle cx={x(i)} cy={y(p.value)} r="4"/><text x={x(i)} y={y(p.value) - 14} textAnchor="middle">{format(p.value)}</text></>
: <text x={x(i)} y="172" textAnchor="middle">—</text>}
<text x={x(i)} y="218" textAnchor="middle" className="case-chart-label">{Array.from(p.label).length > 12 ? `${Array.from(p.label).slice(0, 11).join('')}…` : p.label}</text>
</g>)}
</svg>
</div>
<details><summary>{zh ? '查看数值' : 'Read values'}</summary><dl>{series.points.map((p, i) => <div key={i}><dt>{p.label}</dt><dd>{p.value === null ? (zh ? '未记录' : 'Not recorded') : `${format(p.value)} ${series.unit}`}</dd></div>)}</dl></details>
<ol className="case-series-points" role="list">
{series.points.map((point, i) => {
const end = point.value === null ? null : position(point.value);
return <li key={i}>
<span className="case-chart-label">{point.label}</span>
<span className="case-chart-value">{point.value === null ? (zh ? '未记录' : 'Not recorded') : format(point.value)}</span>
<span className="case-chart-track" aria-hidden="true">
<span className="case-chart-zero" style={{insetInlineStart: `${zero}%`}}/>
{end !== null ? <>
<span className="case-chart-bar" style={{insetInlineStart: `${Math.min(end, zero)}%`, inlineSize: `${Math.abs(end - zero)}%`}}/>
<span className="case-chart-mark" style={{insetInlineStart: `${end}%`}}/>
</> : null}
</span>
</li>;
})}
</ol>
</figure>;
}

Expand Down
Loading