niceeval 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +55 -0
- package/dist/agents/types.d.ts +321 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
- package/dist/report/built-ins/experiment-comparison.js +13 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +180 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +600 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +26 -0
- package/dist/report/format.js +109 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +94 -0
- package/dist/report/locale.js +183 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +19 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +11 -0
- package/dist/report/react/ExperimentList.js +67 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +96 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +398 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +541 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +408 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +7 -7
- package/docs-site/zh/concepts/assert.mdx +19 -18
- package/docs-site/zh/concepts/evals.mdx +12 -11
- package/docs-site/zh/concepts/experiment.mdx +2 -2
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +3 -3
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -1
- package/docs-site/zh/guides/fixtures.mdx +4 -2
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +4 -4
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +61 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
- package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
- package/docs-site/zh/guides/viewing-results.mdx +200 -79
- package/docs-site/zh/guides/write-experiment.mdx +9 -1
- package/docs-site/zh/guides/write-send.mdx +4 -3
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +15 -13
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +9 -3
- package/docs-site/zh/reference/expect.mdx +13 -8
- package/package.json +10 -6
- package/src/agents/bub.ts +129 -42
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +9 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +59 -3
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +23 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +8 -8
- package/src/define.ts +11 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +33 -15
- package/src/i18n/zh-CN.ts +37 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +640 -0
- package/src/report/built-ins/experiment-comparison.tsx +19 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +225 -50
- package/src/report/compute.ts +335 -54
- package/src/report/dual-render.test.tsx +1129 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +35 -2
- package/src/report/index.ts +47 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +229 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +113 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +220 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +126 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +168 -0
- package/src/report/react/fixtures.ts +169 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +21 -6
- package/src/report/react/render.test.tsx +180 -31
- package/src/report/react/styles.css +432 -213
- package/src/report/report.test.ts +398 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +344 -96
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +184 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +48 -13
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +24 -24
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +119 -95
- package/src/show/render.ts +462 -159
- package/src/show/show.test.ts +471 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// MetricLine:趋势线——x 是 experiment 声明的 flag(有序变量),每个系列一条线。
|
|
2
2
|
// 与 MetricScatter 的分工:scatter 的两轴都是测出来的指标(找 frontier),
|
|
3
3
|
// line 的 x 是你配置的变量(看趋势)。x 轴正常升序;y 轴向随 better;
|
|
4
|
-
// x 或 y 缺数据的点不画,注脚如实报数;每个点带 <title> hover
|
|
4
|
+
// x 或 y 缺数据的点不画,注脚如实报数;每个点带 <title> hover(enhance.js 在场时
|
|
5
|
+
// 升级为样式化 tooltip),可经 pointHref 下钻。配色走类名(nre-series-cN)由 CSS
|
|
6
|
+
// 上色,深色主题跟随,不留内联 hex。
|
|
5
7
|
|
|
6
8
|
import type { ReactElement } from "react";
|
|
7
9
|
import type { LineData } from "../types.ts";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
+
import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
|
|
11
|
+
import { seriesClassForKey } from "./colors.ts";
|
|
12
|
+
import { cx } from "./format.ts";
|
|
10
13
|
|
|
11
14
|
const WIDTH = 640;
|
|
12
15
|
const HEIGHT = 360;
|
|
@@ -39,25 +42,28 @@ export function MetricLine({
|
|
|
39
42
|
data,
|
|
40
43
|
pointHref,
|
|
41
44
|
className,
|
|
45
|
+
locale = DEFAULT_REPORT_LOCALE,
|
|
42
46
|
}: {
|
|
43
47
|
data: LineData;
|
|
44
48
|
pointHref?: (row: LineData["rows"][number]) => string;
|
|
45
49
|
className?: string;
|
|
50
|
+
locale?: ReportLocale;
|
|
46
51
|
}): ReactElement {
|
|
47
52
|
const missing = data.rows.filter((r) => r.x === null || r.y.value === null);
|
|
48
53
|
const drawableRows = data.rows.filter((r) => r.x !== null && r.y.value !== null);
|
|
54
|
+
const yLabel = resolveMetricLabel(data.y.label, locale, data.y.key);
|
|
49
55
|
|
|
50
56
|
const missingNote =
|
|
51
57
|
missing.length > 0 ? (
|
|
52
58
|
<p className="nre-line-missing" title={missing.map((r) => r.key).join(", ")}>
|
|
53
|
-
{
|
|
59
|
+
{countText(locale, "pointsMissing", missing.length)}
|
|
54
60
|
</p>
|
|
55
61
|
) : null;
|
|
56
62
|
|
|
57
63
|
if (drawableRows.length === 0) {
|
|
58
64
|
return (
|
|
59
65
|
<figure className={cx("nre", "nre-metric-line", className)}>
|
|
60
|
-
<p className="nre-missing">{
|
|
66
|
+
<p className="nre-missing">{localeText(locale, "cell.missing")}</p>
|
|
61
67
|
{missingNote}
|
|
62
68
|
</figure>
|
|
63
69
|
);
|
|
@@ -77,7 +83,7 @@ export function MetricLine({
|
|
|
77
83
|
series: r.series,
|
|
78
84
|
xValue: r.x as number,
|
|
79
85
|
yValue: r.y.value as number,
|
|
80
|
-
title: `${r.key}\n${data.x.label}: ${r.xDisplay}\n${
|
|
86
|
+
title: `${r.key}\n${data.x.label}: ${r.xDisplay}\n${yLabel}: ${r.y.display}(${r.y.samples}/${r.y.total})`,
|
|
81
87
|
px: xScale.scale(r.x as number),
|
|
82
88
|
py: yScale.scale(r.y.value as number),
|
|
83
89
|
}));
|
|
@@ -108,7 +114,7 @@ export function MetricLine({
|
|
|
108
114
|
className="nre-line-svg"
|
|
109
115
|
viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
|
|
110
116
|
role="img"
|
|
111
|
-
aria-label={`${
|
|
117
|
+
aria-label={`${yLabel} by ${data.x.label}`}
|
|
112
118
|
>
|
|
113
119
|
<rect
|
|
114
120
|
className="nre-line-plot"
|
|
@@ -117,9 +123,8 @@ export function MetricLine({
|
|
|
117
123
|
width={PLOT.right - PLOT.left}
|
|
118
124
|
height={PLOT.bottom - PLOT.top}
|
|
119
125
|
fill="none"
|
|
120
|
-
stroke="#d4d4d4"
|
|
121
126
|
/>
|
|
122
|
-
<text className="nre-line-xlabel" x={(PLOT.left + PLOT.right) / 2} y={HEIGHT - 8} textAnchor="middle"
|
|
127
|
+
<text className="nre-line-xlabel" x={(PLOT.left + PLOT.right) / 2} y={HEIGHT - 8} textAnchor="middle">
|
|
123
128
|
{data.x.label}
|
|
124
129
|
{data.x.unit ? `(${data.x.unit})` : ""}
|
|
125
130
|
</text>
|
|
@@ -128,42 +133,38 @@ export function MetricLine({
|
|
|
128
133
|
x={16}
|
|
129
134
|
y={(PLOT.top + PLOT.bottom) / 2}
|
|
130
135
|
textAnchor="middle"
|
|
131
|
-
fontSize={12}
|
|
132
|
-
fill="#525252"
|
|
133
136
|
transform={`rotate(-90 16 ${(PLOT.top + PLOT.bottom) / 2})`}
|
|
134
137
|
>
|
|
135
|
-
{
|
|
138
|
+
{yLabel}
|
|
136
139
|
{data.y.unit ? `(${data.y.unit})` : ""}
|
|
137
140
|
</text>
|
|
138
141
|
|
|
139
142
|
{xTicks.map((v) => (
|
|
140
|
-
<text key={`x${v}`} className="nre-line-tick" x={xScale.scale(v)} y={PLOT.bottom + 16} textAnchor="middle"
|
|
143
|
+
<text key={`x${v}`} className="nre-line-tick" x={xScale.scale(v)} y={PLOT.bottom + 16} textAnchor="middle">
|
|
141
144
|
{xDisplayFor(v)}
|
|
142
145
|
</text>
|
|
143
146
|
))}
|
|
144
147
|
{yTicks.map((v) => (
|
|
145
|
-
<text key={`y${v}`} className="nre-line-tick" x={PLOT.left - 6} y={yScale.scale(v) + 4} textAnchor="end"
|
|
148
|
+
<text key={`y${v}`} className="nre-line-tick" x={PLOT.left - 6} y={yScale.scale(v) + 4} textAnchor="end">
|
|
146
149
|
{yDisplayFor(v)}
|
|
147
150
|
</text>
|
|
148
151
|
))}
|
|
149
152
|
|
|
150
153
|
{seriesOrder.map((series) => {
|
|
151
154
|
const list = bySeries.get(series)!;
|
|
152
|
-
const
|
|
155
|
+
const seriesClass = series === "" ? "nre-series-none" : seriesClassForKey(series);
|
|
153
156
|
const labelAt = list.reduce((a, b) => (b.px > a.px ? b : a));
|
|
154
157
|
return (
|
|
155
|
-
<g key={series || "(single)"} className="nre-line-series" data-series={series || undefined}>
|
|
158
|
+
<g key={series || "(single)"} className={cx("nre-line-series", seriesClass)} data-series={series || undefined}>
|
|
156
159
|
{list.length > 1 && (
|
|
157
160
|
<polyline
|
|
158
161
|
className="nre-line-path"
|
|
159
162
|
points={list.map((p) => `${p.px},${p.py}`).join(" ")}
|
|
160
163
|
fill="none"
|
|
161
|
-
stroke={color}
|
|
162
|
-
strokeWidth={1.5}
|
|
163
164
|
/>
|
|
164
165
|
)}
|
|
165
166
|
{series !== "" && (
|
|
166
|
-
<text className="nre-line-series-label" x={labelAt.px + 8} y={labelAt.py + 4}
|
|
167
|
+
<text className="nre-line-series-label" x={labelAt.px + 8} y={labelAt.py + 4}>
|
|
167
168
|
{series}
|
|
168
169
|
</text>
|
|
169
170
|
)}
|
|
@@ -174,12 +175,11 @@ export function MetricLine({
|
|
|
174
175
|
{points.map((p, i) => {
|
|
175
176
|
const circle = (
|
|
176
177
|
<circle
|
|
177
|
-
className="nre-line-point"
|
|
178
|
+
className={cx("nre-line-point", p.series !== undefined ? seriesClassForKey(p.series) : "nre-series-none")}
|
|
178
179
|
data-key={p.key}
|
|
179
180
|
cx={p.px}
|
|
180
181
|
cy={p.py}
|
|
181
182
|
r={4}
|
|
182
|
-
fill={p.series !== undefined ? colorHexForKey(p.series) : "#525252"}
|
|
183
183
|
>
|
|
184
184
|
<title>{p.title}</title>
|
|
185
185
|
</circle>
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// 是格子里的普通 <a>,不 hydrate 也能点。
|
|
6
6
|
|
|
7
7
|
import type { ReactElement } from "react";
|
|
8
|
-
import type {
|
|
8
|
+
import type { MatrixData } from "../types.ts";
|
|
9
|
+
import type { AttemptLocator } from "../../results/locator.ts";
|
|
10
|
+
import { DEFAULT_REPORT_LOCALE, resolveMetricLabel, type ReportLocale } from "../locale.ts";
|
|
9
11
|
import { MetricCellView } from "./cell.tsx";
|
|
10
12
|
import { colorClassForKey } from "./colors.ts";
|
|
11
13
|
import { cx } from "./format.ts";
|
|
@@ -14,10 +16,12 @@ export function MetricMatrix({
|
|
|
14
16
|
data,
|
|
15
17
|
attemptHref,
|
|
16
18
|
className,
|
|
19
|
+
locale = DEFAULT_REPORT_LOCALE,
|
|
17
20
|
}: {
|
|
18
21
|
data: MatrixData;
|
|
19
|
-
attemptHref?: (
|
|
22
|
+
attemptHref?: (locator: AttemptLocator) => string;
|
|
20
23
|
className?: string;
|
|
24
|
+
locale?: ReportLocale;
|
|
21
25
|
}): ReactElement {
|
|
22
26
|
// 稀疏 cells → 首次出现顺序的行/列键 + 查找表;组件只整理形状,不碰数值
|
|
23
27
|
const rowKeys: string[] = [];
|
|
@@ -33,7 +37,7 @@ export function MetricMatrix({
|
|
|
33
37
|
return (
|
|
34
38
|
<table className={cx("nre", "nre-metric-matrix", className)}>
|
|
35
39
|
<caption className="nre-matrix-caption">
|
|
36
|
-
{data.metric.label}
|
|
40
|
+
{resolveMetricLabel(data.metric.label, locale, data.metric.key)}
|
|
37
41
|
{data.metric.unit && <span className="nre-unit">({data.metric.unit})</span>}
|
|
38
42
|
<span className="nre-matrix-axes">
|
|
39
43
|
{data.rows} × {data.columns}
|
|
@@ -63,7 +67,7 @@ export function MetricMatrix({
|
|
|
63
67
|
return (
|
|
64
68
|
<td key={column} className={cx("nre-td", !cell && "nre-td-empty")}>
|
|
65
69
|
{/* 稀疏格子:没有样本就空着(数据里不存在),不是 0 也不是缺数据文案 */}
|
|
66
|
-
{cell ? <MetricCellView cell={cell} attemptHref={attemptHref} /> : null}
|
|
70
|
+
{cell ? <MetricCellView cell={cell} attemptHref={attemptHref} locale={locale} /> : null}
|
|
67
71
|
</td>
|
|
68
72
|
);
|
|
69
73
|
})}
|
|
@@ -1,54 +1,68 @@
|
|
|
1
|
-
// MetricScatter:质量 × 成本 frontier 的积木,内联 SVG
|
|
1
|
+
// MetricScatter:质量 × 成本 frontier 的积木,内联 SVG、零图表库、零 hooks。
|
|
2
2
|
// 轴向随 better:"lower" 的轴反向画,「好」的角落恒在右上(成本轴 $20 → $0 就是这么来的);
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// niceTicks 刻度 + 网格线;每个点直接标注(防撞布局,被挤开时补 leader line);
|
|
4
|
+
// 同系列的点按 x 值排序连线,系列图例列在图下。x 或 y 为 null 的点不画,
|
|
5
|
+
// 底部注脚如实报「n 个点缺数据」;hover 信息退化为 SVG <title>,不 hydrate 也在
|
|
6
|
+
// (enhance.js 在场时升级为样式化 tooltip)。配色走类名(nre-series-cN)由 CSS 上色,
|
|
7
|
+
// 深色主题下图表随令牌切换,不留内联 hex。
|
|
5
8
|
|
|
6
9
|
import type { ReactElement } from "react";
|
|
7
10
|
import type { MetricColumn, ScatterData } from "../types.ts";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
11
|
+
import { formatMetricValue } from "../format.ts";
|
|
12
|
+
import { DEFAULT_REPORT_LOCALE, countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
|
|
13
|
+
import { layoutLabelOffsets, niceTicks } from "./chart-math.ts";
|
|
14
|
+
import { colorClassForKey, seriesClassForKey } from "./colors.ts";
|
|
15
|
+
import { cx } from "./format.ts";
|
|
10
16
|
|
|
11
|
-
// 画布与边距:右侧留白给系列名,底部给 x 轴标签
|
|
12
17
|
const WIDTH = 640;
|
|
13
|
-
const HEIGHT =
|
|
14
|
-
const
|
|
18
|
+
const HEIGHT = 360;
|
|
19
|
+
const MARGIN = { top: 26, right: 24, bottom: 46, left: 62 };
|
|
20
|
+
const PLOT_W = WIDTH - MARGIN.left - MARGIN.right;
|
|
21
|
+
const PLOT_H = HEIGHT - MARGIN.top - MARGIN.bottom;
|
|
15
22
|
|
|
16
23
|
/** 可画的点:x/y 都有值。组件内部的整理结果,不改数据。 */
|
|
17
24
|
interface DrawablePoint {
|
|
18
25
|
key: string;
|
|
19
26
|
series?: string;
|
|
27
|
+
label: string;
|
|
20
28
|
xValue: number;
|
|
21
29
|
yValue: number;
|
|
22
|
-
xDisplay: string;
|
|
23
|
-
yDisplay: string;
|
|
24
30
|
title: string;
|
|
25
31
|
px: number;
|
|
26
32
|
py: number;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
/**
|
|
35
|
+
/** 点的直接标签:experiment id 的末段(完整 id 在 <title> 里)。 */
|
|
36
|
+
function pointLabel(key: string): string {
|
|
37
|
+
return key.split("/").filter(Boolean).at(-1) ?? key;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 一根轴:niceTicks 撑出整齐的值域,值 → 像素做线性映射;
|
|
42
|
+
* better: "lower" 时反向,好的一端固定在右 / 上。
|
|
43
|
+
*/
|
|
30
44
|
function axisScale(values: number[], better: MetricColumn["better"], pixelLo: number, pixelHi: number) {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const span = hi - lo || 2;
|
|
35
|
-
const padded = { lo: lo - (hi - lo ? span * 0.08 : 1), hi: hi + (hi - lo ? span * 0.08 : 1) };
|
|
45
|
+
const ticks = niceTicks(Math.min(...values), Math.max(...values), 5);
|
|
46
|
+
const lo = ticks[0];
|
|
47
|
+
const hi = ticks[ticks.length - 1];
|
|
36
48
|
const scale = (v: number) => {
|
|
37
|
-
let t = (v -
|
|
49
|
+
let t = (v - lo) / (hi - lo || 1);
|
|
38
50
|
if (better === "lower") t = 1 - t; // 反向:值越低越靠「好」的一端
|
|
39
51
|
return pixelLo + t * (pixelHi - pixelLo);
|
|
40
52
|
};
|
|
41
|
-
return {
|
|
53
|
+
return { ticks, scale };
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
export function MetricScatter({
|
|
45
57
|
data,
|
|
46
58
|
pointHref,
|
|
47
59
|
className,
|
|
60
|
+
locale = DEFAULT_REPORT_LOCALE,
|
|
48
61
|
}: {
|
|
49
62
|
data: ScatterData;
|
|
50
63
|
pointHref?: (row: ScatterData["rows"][number]) => string;
|
|
51
64
|
className?: string;
|
|
65
|
+
locale?: ReportLocale;
|
|
52
66
|
}): ReactElement {
|
|
53
67
|
const missing = data.rows.filter((r) => r.x.value === null || r.y.value === null);
|
|
54
68
|
const drawableRows = data.rows.filter((r) => r.x.value !== null && r.y.value !== null);
|
|
@@ -56,23 +70,29 @@ export function MetricScatter({
|
|
|
56
70
|
const missingNote =
|
|
57
71
|
missing.length > 0 ? (
|
|
58
72
|
<p className="nre-scatter-missing" title={missing.map((r) => r.key).join(", ")}>
|
|
59
|
-
{
|
|
73
|
+
{countText(locale, "pointsMissing", missing.length)}
|
|
60
74
|
</p>
|
|
61
75
|
) : null;
|
|
62
76
|
|
|
63
|
-
|
|
77
|
+
const xLabel = resolveMetricLabel(data.x.label, locale, data.x.key);
|
|
78
|
+
const yLabel = resolveMetricLabel(data.y.label, locale, data.y.key);
|
|
79
|
+
|
|
80
|
+
// 0 个可画点:x/y 指标没有可用数据 —— 明确说缺哪两个指标,不画一张空坐标系
|
|
81
|
+
// (与表格的「绝不画 0」同态度)。
|
|
64
82
|
if (drawableRows.length === 0) {
|
|
65
83
|
return (
|
|
66
84
|
<figure className={cx("nre", "nre-metric-scatter", className)}>
|
|
67
|
-
<p className="nre-missing">{
|
|
85
|
+
<p className="nre-scatter-empty nre-missing">{localeText(locale, "scatter.noData", { x: xLabel, y: yLabel })}</p>
|
|
68
86
|
{missingNote}
|
|
69
87
|
</figure>
|
|
70
88
|
);
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
const
|
|
91
|
+
const axisLabel = (label: string, col: MetricColumn) => `${label}${col.unit ? `(${col.unit})` : ""}`;
|
|
92
|
+
|
|
93
|
+
const xScale = axisScale(drawableRows.map((r) => r.x.value as number), data.x.better, MARGIN.left, MARGIN.left + PLOT_W);
|
|
74
94
|
// y 像素轴向下增长:better:"higher" 高值在上 → 映射到 [bottom, top];"lower" 由 axisScale 反向后同样落到上方
|
|
75
|
-
const yScale = axisScale(drawableRows.map((r) => r.y.value as number), data.y.better,
|
|
95
|
+
const yScale = axisScale(drawableRows.map((r) => r.y.value as number), data.y.better, MARGIN.top + PLOT_H, MARGIN.top);
|
|
76
96
|
|
|
77
97
|
const points: DrawablePoint[] = drawableRows.map((r) => {
|
|
78
98
|
const xValue = r.x.value as number;
|
|
@@ -80,12 +100,12 @@ export function MetricScatter({
|
|
|
80
100
|
return {
|
|
81
101
|
key: r.key,
|
|
82
102
|
series: r.series,
|
|
103
|
+
label: pointLabel(r.key),
|
|
83
104
|
xValue,
|
|
84
105
|
yValue,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
title: `${r.key}\n${data.x.label}: ${r.x.display}(${r.x.samples}/${r.x.total})\n${data.y.label}: ${r.y.display}(${r.y.samples}/${r.y.total})`,
|
|
106
|
+
// hover 内容:experiment(点键)+ 系列(series,ExperimentComparison 传的是 agent 维度,
|
|
107
|
+
// 有则加一行;无系列的散点没有这行)+ 两轴 display 与 samples/total(docs/feature/reports/library.md「MetricScatter」行为清单)
|
|
108
|
+
title: `${r.key}${r.series !== undefined ? `\n${r.series}` : ""}\n${xLabel}: ${r.x.display}(${r.x.samples}/${r.x.total})\n${yLabel}: ${r.y.display}(${r.y.samples}/${r.y.total})`,
|
|
89
109
|
px: xScale.scale(xValue),
|
|
90
110
|
py: yScale.scale(yValue),
|
|
91
111
|
};
|
|
@@ -104,14 +124,12 @@ export function MetricScatter({
|
|
|
104
124
|
}
|
|
105
125
|
for (const list of bySeries.values()) list.sort((a, b) => a.xValue - b.xValue);
|
|
106
126
|
|
|
107
|
-
//
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const axisLabel = (col: MetricColumn) => `${col.label}${col.unit ? `(${col.unit})` : ""}`;
|
|
127
|
+
// 直接标签的防撞布局:靠右的点把标签锚到左侧,重叠的往下推、补 leader line
|
|
128
|
+
const positions = points.map((p) => {
|
|
129
|
+
const anchorLeft = p.px >= MARGIN.left + PLOT_W * 0.72;
|
|
130
|
+
return { cx: p.px, cy: p.py, anchorLeft, width: p.label.length * 6.4 + 10 };
|
|
131
|
+
});
|
|
132
|
+
const labelOffsets = layoutLabelOffsets(positions);
|
|
115
133
|
|
|
116
134
|
return (
|
|
117
135
|
<figure className={cx("nre", "nre-metric-scatter", className)}>
|
|
@@ -119,98 +137,110 @@ export function MetricScatter({
|
|
|
119
137
|
className="nre-scatter-svg"
|
|
120
138
|
viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
|
|
121
139
|
role="img"
|
|
122
|
-
aria-label={`${axisLabel(data.x)} × ${axisLabel(data.y)}`}
|
|
140
|
+
aria-label={`${axisLabel(xLabel, data.x)} × ${axisLabel(yLabel, data.y)}`}
|
|
123
141
|
>
|
|
124
|
-
{/*
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
{/* 网格:niceTicks 的整齐刻度线,颜色走 CSS(var(--line)) */}
|
|
143
|
+
<g className="nre-scatter-grid">
|
|
144
|
+
{yScale.ticks.map((tick) => (
|
|
145
|
+
<line key={`gy${tick}`} x1={MARGIN.left} x2={MARGIN.left + PLOT_W} y1={yScale.scale(tick)} y2={yScale.scale(tick)} />
|
|
146
|
+
))}
|
|
147
|
+
{xScale.ticks.map((tick) => (
|
|
148
|
+
<line key={`gx${tick}`} y1={MARGIN.top} y2={MARGIN.top + PLOT_H} x1={xScale.scale(tick)} x2={xScale.scale(tick)} />
|
|
149
|
+
))}
|
|
150
|
+
</g>
|
|
151
|
+
|
|
134
152
|
{/* 「好」的角落恒在右上:轴向已按 better 反转,这里只是把这句契约写在图上 */}
|
|
135
|
-
<text className="nre-scatter-better-hint" x={
|
|
136
|
-
|
|
153
|
+
<text className="nre-scatter-better-hint" x={MARGIN.left + PLOT_W - 6} y={MARGIN.top + 14} textAnchor="end">
|
|
154
|
+
{localeText(locale, "scatter.betterHint")}
|
|
137
155
|
</text>
|
|
138
156
|
|
|
157
|
+
{/* 刻度:已格式化的整齐值(formatMetricValue 与计算侧同一套) */}
|
|
158
|
+
<g className="nre-scatter-axis nre-scatter-axis-y">
|
|
159
|
+
{yScale.ticks.map((tick) => (
|
|
160
|
+
<text key={`ay${tick}`} className="nre-scatter-tick" x={MARGIN.left - 8} y={yScale.scale(tick) + 3} textAnchor="end">
|
|
161
|
+
{formatMetricValue(tick, data.y.unit)}
|
|
162
|
+
</text>
|
|
163
|
+
))}
|
|
164
|
+
</g>
|
|
165
|
+
<g className="nre-scatter-axis nre-scatter-axis-x">
|
|
166
|
+
{xScale.ticks.map((tick) => (
|
|
167
|
+
<text key={`ax${tick}`} className="nre-scatter-tick" x={xScale.scale(tick)} y={MARGIN.top + PLOT_H + 16} textAnchor="middle">
|
|
168
|
+
{formatMetricValue(tick, data.x.unit)}
|
|
169
|
+
</text>
|
|
170
|
+
))}
|
|
171
|
+
</g>
|
|
172
|
+
|
|
139
173
|
{/* 轴标签 */}
|
|
140
|
-
<text className="nre-scatter-xlabel" x={
|
|
141
|
-
{axisLabel(data.x)}
|
|
174
|
+
<text className="nre-scatter-xlabel" x={MARGIN.left + PLOT_W / 2} y={HEIGHT - 8} textAnchor="middle">
|
|
175
|
+
{axisLabel(xLabel, data.x)}
|
|
142
176
|
</text>
|
|
143
177
|
<text
|
|
144
178
|
className="nre-scatter-ylabel"
|
|
145
|
-
x={
|
|
146
|
-
y={
|
|
179
|
+
x={14}
|
|
180
|
+
y={MARGIN.top + PLOT_H / 2}
|
|
147
181
|
textAnchor="middle"
|
|
148
|
-
|
|
149
|
-
fill="#525252"
|
|
150
|
-
transform={`rotate(-90 16 ${(PLOT.top + PLOT.bottom) / 2})`}
|
|
182
|
+
transform={`rotate(-90 14 ${MARGIN.top + PLOT_H / 2})`}
|
|
151
183
|
>
|
|
152
|
-
{axisLabel(data.y)}
|
|
184
|
+
{axisLabel(yLabel, data.y)}
|
|
153
185
|
</text>
|
|
154
186
|
|
|
155
|
-
{/*
|
|
156
|
-
{xTicks.map((v) => (
|
|
157
|
-
<text key={`x${v}`} className="nre-scatter-tick" x={xScale.scale(v)} y={PLOT.bottom + 16} textAnchor="middle" fontSize={11} fill="#737373">
|
|
158
|
-
{displayFor("x", v)}
|
|
159
|
-
</text>
|
|
160
|
-
))}
|
|
161
|
-
{yTicks.map((v) => (
|
|
162
|
-
<text key={`y${v}`} className="nre-scatter-tick" x={PLOT.left - 6} y={yScale.scale(v) + 4} textAnchor="end" fontSize={11} fill="#737373">
|
|
163
|
-
{displayFor("y", v)}
|
|
164
|
-
</text>
|
|
165
|
-
))}
|
|
166
|
-
|
|
167
|
-
{/* 系列连线 + 线旁的系列名(标在视觉上最靠右的点旁) */}
|
|
187
|
+
{/* 系列连线:类名上色(nre-series-cN),深色主题跟随 */}
|
|
168
188
|
{seriesOrder.map((series) => {
|
|
169
189
|
const list = bySeries.get(series)!;
|
|
170
|
-
|
|
171
|
-
const labelAt = list.reduce((a, b) => (b.px > a.px ? b : a));
|
|
190
|
+
if (list.length < 2) return null;
|
|
172
191
|
return (
|
|
173
|
-
<
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
strokeWidth={1.5}
|
|
181
|
-
/>
|
|
182
|
-
)}
|
|
183
|
-
<text className="nre-scatter-series-label" x={labelAt.px + 8} y={labelAt.py + 4} fontSize={12} fill={color}>
|
|
184
|
-
{series}
|
|
185
|
-
</text>
|
|
186
|
-
</g>
|
|
192
|
+
<polyline
|
|
193
|
+
key={series}
|
|
194
|
+
className={cx("nre-scatter-line", seriesClassForKey(series))}
|
|
195
|
+
data-series={series}
|
|
196
|
+
points={list.map((p) => `${p.px},${p.py}`).join(" ")}
|
|
197
|
+
fill="none"
|
|
198
|
+
/>
|
|
187
199
|
);
|
|
188
200
|
})}
|
|
189
201
|
|
|
190
|
-
{/*
|
|
202
|
+
{/* 点:g 内带 <title>(无 JS 的原生 hover)、直接标签与 leader line;
|
|
203
|
+
pointHref 时包普通 <a>,静态导出也能下钻 */}
|
|
191
204
|
{points.map((p, i) => {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
const { anchorLeft } = positions[i];
|
|
206
|
+
const labelX = p.px + (anchorLeft ? -10 : 10);
|
|
207
|
+
const labelY = p.py + 4 + labelOffsets[i];
|
|
208
|
+
const group = (
|
|
209
|
+
<g
|
|
210
|
+
className={cx("nre-scatter-point", p.series !== undefined ? seriesClassForKey(p.series) : "nre-series-none")}
|
|
195
211
|
data-key={p.key}
|
|
196
|
-
cx={p.px}
|
|
197
|
-
cy={p.py}
|
|
198
|
-
r={4.5}
|
|
199
|
-
fill={p.series !== undefined ? colorHexForKey(p.series) : "#525252"}
|
|
200
212
|
>
|
|
201
213
|
<title>{p.title}</title>
|
|
202
|
-
|
|
214
|
+
{/* 标签被挤开时补一条 leader line,避免脱离原点看不出对应关系 */}
|
|
215
|
+
{labelOffsets[i] !== 0 && <line className="nre-leader" x1={p.px} y1={p.py} x2={labelX} y2={labelY - 4} />}
|
|
216
|
+
<circle className="nre-scatter-hit" cx={p.px} cy={p.py} r={12} />
|
|
217
|
+
<circle className="nre-scatter-dot" cx={p.px} cy={p.py} r={4.5} />
|
|
218
|
+
<text className="nre-scatter-point-label" x={labelX} y={labelY} textAnchor={anchorLeft ? "end" : "start"}>
|
|
219
|
+
{p.label}
|
|
220
|
+
</text>
|
|
221
|
+
</g>
|
|
203
222
|
);
|
|
204
223
|
const row = drawableRows[i];
|
|
205
224
|
return pointHref ? (
|
|
206
225
|
<a key={p.key} className="nre-scatter-point-link" href={pointHref(row)}>
|
|
207
|
-
{
|
|
226
|
+
{group}
|
|
208
227
|
</a>
|
|
209
228
|
) : (
|
|
210
|
-
<g key={p.key}>{
|
|
229
|
+
<g key={p.key}>{group}</g>
|
|
211
230
|
);
|
|
212
231
|
})}
|
|
213
232
|
</svg>
|
|
233
|
+
|
|
234
|
+
{/* 系列图例:同键同色(与其它块的稳定散列一致) */}
|
|
235
|
+
{seriesOrder.length > 0 && (
|
|
236
|
+
<figcaption className="nre-scatter-legend">
|
|
237
|
+
{seriesOrder.map((series) => (
|
|
238
|
+
<span key={series} className={cx("nre-legend-key", "nre-key", colorClassForKey(series))}>
|
|
239
|
+
{series}
|
|
240
|
+
</span>
|
|
241
|
+
))}
|
|
242
|
+
</figcaption>
|
|
243
|
+
)}
|
|
214
244
|
{missingNote}
|
|
215
245
|
</figure>
|
|
216
246
|
);
|