niceeval 0.11.4-canary.21 → 0.12.0
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/dist/report/assets/series-encoding.d.ts +36 -0
- package/dist/report/assets/series-encoding.js +104 -0
- package/dist/report/components/entity-lists/compute.js +5 -17
- package/dist/report/components/entity-lists/content.d.ts +2 -0
- package/dist/report/components/entity-lists/content.js +22 -7
- package/dist/report/definition/cell.d.ts +4 -0
- package/dist/report/definition/cell.js +5 -1
- package/dist/report/definition/primitives/chart.js +125 -25
- package/dist/report/definition/primitives.js +3 -2
- package/dist/report/definition/tree.d.ts +5 -2
- package/dist/report/definition/tree.js +1 -0
- package/dist/report/index.d.ts +1 -1
- package/dist/report/model/aggregate.d.ts +10 -1
- package/dist/report/model/aggregate.js +24 -0
- package/dist/report/model/locale.d.ts +0 -1
- package/dist/report/model/locale.js +0 -2
- package/dist/report/model/types.d.ts +8 -0
- package/dist/report/presentation.d.ts +53 -6
- package/dist/report/presentation.js +40 -5
- package/dist/report/react/index.d.ts +1 -1
- package/dist/report/slices/compute.js +52 -4
- package/dist/report/slices/content.d.ts +2 -1
- package/dist/report/slices/content.js +48 -15
- package/dist/report/slices/validate.js +23 -0
- package/package.json +1 -1
- package/src/report/assets/series-encoding.tsx +177 -0
- package/src/report/assets/styles.css +47 -5
- package/src/report/components/compute.test.ts +15 -9
- package/src/report/components/entity-lists/compute.ts +5 -19
- package/src/report/components/entity-lists/content.test.ts +13 -8
- package/src/report/components/entity-lists/content.ts +24 -7
- package/src/report/definition/cell.ts +8 -1
- package/src/report/definition/primitives/chart.tsx +197 -32
- package/src/report/definition/primitives.tsx +8 -3
- package/src/report/definition/tree.ts +6 -2
- package/src/report/index.ts +11 -1
- package/src/report/model/aggregate.ts +26 -0
- package/src/report/model/locale.ts +0 -2
- package/src/report/model/types.ts +8 -0
- package/src/report/presentation.test.tsx +179 -19
- package/src/report/presentation.ts +114 -11
- package/src/report/react/index.tsx +11 -1
- package/src/report/slices/compute.ts +59 -4
- package/src/report/slices/content.ts +55 -16
- package/src/report/slices/delta-table.test.ts +138 -3
- package/src/report/slices/validate.test.ts +29 -0
- package/src/report/slices/validate.ts +16 -0
- package/src/runner/gate-lease.test.ts +38 -14
- package/src/runner/lock.test.ts +40 -16
- package/src/sandbox/compose.ts +3 -1
- package/src/sandbox/dockerfile-build.ts +4 -1
- package/src/sandbox/runtime.ts +4 -1
- package/src/sandbox/vercel.test.ts +12 -3
- package/src/show/index.ts +10 -9
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// Chart 原语:消费 Dataset,用 x/y 与 <Series> 映射坐标与 mark(docs/feature/reports/components/charts/README.md)。
|
|
2
2
|
|
|
3
|
-
import type { ReactNode } from "react";
|
|
3
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
4
4
|
import type { AttemptLocator } from "../../../record/locator.ts";
|
|
5
|
+
import {
|
|
6
|
+
SeriesPatternDefs,
|
|
7
|
+
seriesClassFromColorVar,
|
|
8
|
+
seriesClassesFromFill,
|
|
9
|
+
} from "../../assets/series-encoding.tsx";
|
|
5
10
|
import { defineComponent, type ReportNode, type TextContext, type WebContext } from "../tree.ts";
|
|
6
11
|
import type { ReportTarget } from "../report.ts";
|
|
7
|
-
import type { DimensionDeclarations } from "../../presentation.ts";
|
|
12
|
+
import type { DimensionDeclarations, DimensionPresentation } from "../../presentation.ts";
|
|
8
13
|
import type { Dataset, DatasetField } from "../../model/types.ts";
|
|
9
14
|
import type { ReportLocale } from "../../model/locale.ts";
|
|
10
15
|
import { countText, localeText, resolveLocalizedText, type ReportLocale as RL } from "../../model/locale.ts";
|
|
@@ -155,12 +160,13 @@ function chartFieldLabel(field: string, meta: DatasetField, locale: RL): string
|
|
|
155
160
|
return meta.unit ? `${base}(${meta.unit})` : base;
|
|
156
161
|
}
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
/** 解析点所属 series 句柄与下标;单系列隐式图不声明视觉身份。 */
|
|
164
|
+
function seriesPresentationOf(
|
|
159
165
|
mapped: MappedSeries[],
|
|
160
166
|
series: MappedSeries,
|
|
161
167
|
point: MappedSeries["points"][number],
|
|
162
168
|
ctx: WebContext,
|
|
163
|
-
):
|
|
169
|
+
): DimensionPresentation | undefined {
|
|
164
170
|
let handle: string;
|
|
165
171
|
let index: number;
|
|
166
172
|
if (series.byField !== undefined && point.seriesValue !== undefined) {
|
|
@@ -168,12 +174,161 @@ function seriesClass(
|
|
|
168
174
|
index = seriesDimensionValues(mapped, series.byField).indexOf(point.seriesValue);
|
|
169
175
|
} else {
|
|
170
176
|
const ids = mapped.filter((item) => !item.hidden && item.byField === undefined).map((item) => item.id);
|
|
171
|
-
if (ids.length < 2) return
|
|
177
|
+
if (ids.length < 2) return undefined;
|
|
172
178
|
handle = IMPLICIT_SERIES_HANDLE;
|
|
173
179
|
index = ids.indexOf(series.id);
|
|
174
180
|
}
|
|
175
|
-
|
|
176
|
-
return
|
|
181
|
+
if (index < 0) return undefined;
|
|
182
|
+
return ctx.dimension(handle).at(index);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** SVG 路径:只挂 series-cN 设 --series;pattern 由 fill 属性 / style 承载。 */
|
|
186
|
+
function seriesColorClass(presentation: DimensionPresentation | undefined): string {
|
|
187
|
+
if (!presentation) return "niceeval-series-none";
|
|
188
|
+
if (presentation.kind === "color") return seriesClassFromColorVar(presentation.color);
|
|
189
|
+
if (presentation.kind !== "series") return "niceeval-series-none";
|
|
190
|
+
// switch 按 mark 收窄;Fill 的 mark 是 "bar"|"area" 联合,if/|| 在部分 TS 版本下剔不干净。
|
|
191
|
+
switch (presentation.mark) {
|
|
192
|
+
case "bar":
|
|
193
|
+
case "area": {
|
|
194
|
+
// 只要色类,不要 HTML 专用的 fill-vN(SVG 用 url(#pattern))。
|
|
195
|
+
const classes = seriesClassesFromFill(presentation.fill).split(" ");
|
|
196
|
+
return classes.find((c) => c.startsWith("niceeval-series-c") || c === "niceeval-series-none") ?? "niceeval-series-none";
|
|
197
|
+
}
|
|
198
|
+
case "line":
|
|
199
|
+
return seriesClassFromColorVar(presentation.stroke);
|
|
200
|
+
case "scatter":
|
|
201
|
+
return seriesClassFromColorVar(presentation.marker.fill);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** HTML 横向柱:色类 + fill-vN 图案类(CSS repeating-linear-gradient 等效 SVG pattern)。 */
|
|
206
|
+
function seriesHtmlBarClass(presentation: DimensionPresentation | undefined): string {
|
|
207
|
+
if (!presentation) return "niceeval-series-none";
|
|
208
|
+
if (presentation.kind === "series" && (presentation.mark === "bar" || presentation.mark === "area")) {
|
|
209
|
+
return seriesClassesFromFill(presentation.fill);
|
|
210
|
+
}
|
|
211
|
+
return seriesColorClass(presentation);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 作者在 `<Series line>` 上显式声明的 dashed/dotted **优先于** 页级 variant 的 strokeDasharray。
|
|
216
|
+
* 未声明时消费 LineSeriesPresentation / FillSeriesPresentation 的 strokeDasharray(空串 = 实线)。
|
|
217
|
+
*/
|
|
218
|
+
function resolveStrokeDasharray(
|
|
219
|
+
series: MappedSeries,
|
|
220
|
+
presentation: DimensionPresentation | undefined,
|
|
221
|
+
): string | undefined {
|
|
222
|
+
if (series.line !== undefined) return lineDash(series.line);
|
|
223
|
+
if (presentation?.kind === "series" && "strokeDasharray" in presentation) {
|
|
224
|
+
return presentation.strokeDasharray || undefined;
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** SVG 柱/面:pattern fill 必须用 style 压过 `.niceeval-chart-bar { fill: var(--series) }`。 */
|
|
230
|
+
function seriesSvgFillStyle(presentation: DimensionPresentation | undefined): CSSProperties | undefined {
|
|
231
|
+
if (presentation?.kind !== "series") return undefined;
|
|
232
|
+
if (presentation.mark !== "bar" && presentation.mark !== "area") return undefined;
|
|
233
|
+
if (!presentation.fill.startsWith("url(")) return undefined;
|
|
234
|
+
return { fill: presentation.fill };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function renderMarkerShape(
|
|
238
|
+
presentation: DimensionPresentation | undefined,
|
|
239
|
+
px: number,
|
|
240
|
+
py: number,
|
|
241
|
+
colorClass: string,
|
|
242
|
+
title: ReactNode,
|
|
243
|
+
): ReactNode {
|
|
244
|
+
const marker =
|
|
245
|
+
presentation?.kind === "series" && (presentation.mark === "scatter" || presentation.mark === "line")
|
|
246
|
+
? presentation.marker
|
|
247
|
+
: undefined;
|
|
248
|
+
if (!marker) {
|
|
249
|
+
return (
|
|
250
|
+
<circle className={cx("niceeval-chart-dot", colorClass)} cx={px} cy={py} r={4.5}>
|
|
251
|
+
{title}
|
|
252
|
+
</circle>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
// path 在 0..12 viewBox;缩放到 ~9px 并居中到 (px, py)。
|
|
256
|
+
return (
|
|
257
|
+
<path
|
|
258
|
+
className={cx("niceeval-chart-dot", colorClass)}
|
|
259
|
+
d={marker.path}
|
|
260
|
+
transform={`translate(${px} ${py}) scale(0.75) translate(-6 -6)`}
|
|
261
|
+
fill={marker.fill}
|
|
262
|
+
stroke="var(--panel)"
|
|
263
|
+
strokeWidth={1.6}
|
|
264
|
+
>
|
|
265
|
+
{title}
|
|
266
|
+
</path>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function legendSwatch(presentation: DimensionPresentation | undefined, mark: MappedSeries["mark"]): ReactNode {
|
|
271
|
+
if (!presentation || presentation.kind !== "series") {
|
|
272
|
+
return <span className={cx("niceeval-chart-legend-swatch", "niceeval-series-none")} />;
|
|
273
|
+
}
|
|
274
|
+
// 图例方块跟 series mark 取义:柱/面用填充图案,线用 dash+marker,散点用 marker 形状。
|
|
275
|
+
if (mark === "bar" || mark === "area") {
|
|
276
|
+
const fillClass =
|
|
277
|
+
presentation.mark === "bar" || presentation.mark === "area"
|
|
278
|
+
? seriesClassesFromFill(presentation.fill)
|
|
279
|
+
: seriesColorClass(presentation);
|
|
280
|
+
return <span className={cx("niceeval-chart-legend-swatch", fillClass)} />;
|
|
281
|
+
}
|
|
282
|
+
if (mark === "line") {
|
|
283
|
+
// 线系图例需要 stroke + marker;presentation 可能是 line 或 scatter(by 切分后 mark 对齐)。
|
|
284
|
+
if (presentation.mark === "line") {
|
|
285
|
+
return (
|
|
286
|
+
<svg className="niceeval-chart-legend-swatch-svg" width="16" height="10" aria-hidden="true">
|
|
287
|
+
<line
|
|
288
|
+
x1="0"
|
|
289
|
+
y1="5"
|
|
290
|
+
x2="16"
|
|
291
|
+
y2="5"
|
|
292
|
+
stroke={presentation.stroke}
|
|
293
|
+
strokeWidth={2}
|
|
294
|
+
strokeDasharray={presentation.strokeDasharray || undefined}
|
|
295
|
+
/>
|
|
296
|
+
<path
|
|
297
|
+
d={presentation.marker.path}
|
|
298
|
+
transform="translate(8 5) scale(0.55) translate(-6 -6)"
|
|
299
|
+
fill={presentation.marker.fill}
|
|
300
|
+
/>
|
|
301
|
+
</svg>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (presentation.mark === "scatter") {
|
|
305
|
+
return (
|
|
306
|
+
<svg className="niceeval-chart-legend-swatch-svg" width="16" height="10" aria-hidden="true">
|
|
307
|
+
<line x1="0" y1="5" x2="16" y2="5" stroke={presentation.marker.fill} strokeWidth={2} />
|
|
308
|
+
<path
|
|
309
|
+
d={presentation.marker.path}
|
|
310
|
+
transform="translate(8 5) scale(0.55) translate(-6 -6)"
|
|
311
|
+
fill={presentation.marker.fill}
|
|
312
|
+
/>
|
|
313
|
+
</svg>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
// fill presentation 配 line mark 的退化:只画色块。
|
|
317
|
+
return <span className={cx("niceeval-chart-legend-swatch", seriesColorClass(presentation))} />;
|
|
318
|
+
}
|
|
319
|
+
// mark === "scatter"
|
|
320
|
+
if (presentation.mark === "scatter" || presentation.mark === "line") {
|
|
321
|
+
return (
|
|
322
|
+
<svg className="niceeval-chart-legend-swatch-svg" width="10" height="10" aria-hidden="true">
|
|
323
|
+
<path
|
|
324
|
+
d={presentation.marker.path}
|
|
325
|
+
transform="translate(5 5) scale(0.7) translate(-6 -6)"
|
|
326
|
+
fill={presentation.marker.fill}
|
|
327
|
+
/>
|
|
328
|
+
</svg>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return <span className={cx("niceeval-chart-legend-swatch", seriesColorClass(presentation))} />;
|
|
177
332
|
}
|
|
178
333
|
|
|
179
334
|
function metricDisplay(
|
|
@@ -210,15 +365,9 @@ function renderLegend(
|
|
|
210
365
|
: implicitIds.indexOf(series.id);
|
|
211
366
|
const presentation = handle ? ctx.dimension(handle).at(index) : undefined;
|
|
212
367
|
const label = presentation?.label ?? (series.label ? resolveLocalizedText(series.label, locale) : series.id);
|
|
213
|
-
const colorClass =
|
|
214
|
-
presentation?.colorIndex === undefined
|
|
215
|
-
? "niceeval-series-none"
|
|
216
|
-
: `niceeval-series-c${presentation.colorIndex - 1}`;
|
|
217
368
|
return (
|
|
218
|
-
<li
|
|
219
|
-
|
|
220
|
-
className={cx("niceeval-chart-legend-item", colorClass)}
|
|
221
|
-
>
|
|
369
|
+
<li key={`${series.id}:${value}`} className="niceeval-chart-legend-item">
|
|
370
|
+
{legendSwatch(presentation, series.mark)}
|
|
222
371
|
{label}
|
|
223
372
|
</li>
|
|
224
373
|
);
|
|
@@ -251,6 +400,8 @@ function renderHorizontalBarsWeb(
|
|
|
251
400
|
options.className,
|
|
252
401
|
)}
|
|
253
402
|
>
|
|
403
|
+
{/* 横向 HTML 柱不引用 pattern,但同页可能另有 SVG 图;defs 全局一份无害。 */}
|
|
404
|
+
<SeriesPatternDefs />
|
|
254
405
|
<div className="niceeval-chart-bars-heading">
|
|
255
406
|
{chartFieldLabel(axes.yField, axes.yMeta, locale)}
|
|
256
407
|
</div>
|
|
@@ -260,7 +411,8 @@ function renderHorizontalBarsWeb(
|
|
|
260
411
|
const label = labels.get(rawLabel) ?? rawLabel;
|
|
261
412
|
const display = metricDisplay(point, "y", axes.yMeta, locale);
|
|
262
413
|
const href = pointHref(point, ctx, options.pointTarget);
|
|
263
|
-
const
|
|
414
|
+
const presentation = seriesPresentationOf(mapped, series, point, ctx);
|
|
415
|
+
const colorClass = seriesHtmlBarClass(presentation);
|
|
264
416
|
const ratio = max > 0 ? Math.max(0, Math.min(1, point.y / max)) : 0;
|
|
265
417
|
const value = (
|
|
266
418
|
<span className="niceeval-chart-bar-value">
|
|
@@ -374,14 +526,18 @@ function renderChartWeb(
|
|
|
374
526
|
const yLabel = chartFieldLabel(axes.yField, axes.yMeta, locale);
|
|
375
527
|
|
|
376
528
|
const drawable = visible.flatMap((series) =>
|
|
377
|
-
series.points.map((point) =>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
529
|
+
series.points.map((point) => {
|
|
530
|
+
const presentation = seriesPresentationOf(mapped, series, point, ctx);
|
|
531
|
+
return {
|
|
532
|
+
...point,
|
|
533
|
+
sourceSeriesId: series.id,
|
|
534
|
+
label: labelByKey.get(point.pointLabel) ?? point.pointLabel,
|
|
535
|
+
px: xScale.scale(point.x),
|
|
536
|
+
py: yScale.scale(point.y),
|
|
537
|
+
presentation,
|
|
538
|
+
seriesClass: seriesColorClass(presentation),
|
|
539
|
+
};
|
|
540
|
+
}),
|
|
385
541
|
);
|
|
386
542
|
|
|
387
543
|
const labels = placePointLabels(
|
|
@@ -391,6 +547,7 @@ function renderChartWeb(
|
|
|
391
547
|
|
|
392
548
|
return (
|
|
393
549
|
<figure className={cx("niceeval-report", "niceeval-chart", "niceeval-chart--scatter", options.className)}>
|
|
550
|
+
<SeriesPatternDefs />
|
|
394
551
|
<svg className="niceeval-chart-svg" viewBox={`0 0 ${WIDTH} ${HEIGHT}`} role="img" aria-label={`${axes.xField} × ${axes.yField}`}>
|
|
395
552
|
{options.grid !== false ? (
|
|
396
553
|
<g className="niceeval-chart-grid">
|
|
@@ -457,7 +614,8 @@ function renderChartWeb(
|
|
|
457
614
|
(series.byField === undefined || p.seriesValue === value),
|
|
458
615
|
);
|
|
459
616
|
const ordered = series.connect ? [...seriesPoints].sort((a, b) => a.x - b.x) : seriesPoints;
|
|
460
|
-
const
|
|
617
|
+
const seriesClassName = ordered[0]?.seriesClass ?? "niceeval-series-none";
|
|
618
|
+
const seriesPresentation = ordered[0]?.presentation;
|
|
461
619
|
const baseline = yScale.scale(0);
|
|
462
620
|
const barGroups = [...new Set(
|
|
463
621
|
visible
|
|
@@ -468,15 +626,18 @@ function renderChartWeb(
|
|
|
468
626
|
const groupIndex = Math.max(0, barGroups.indexOf(barGroup));
|
|
469
627
|
const totalBarWidth = Math.max(8, Math.min(48, PLOT_W / Math.max(1, allPoints.length)));
|
|
470
628
|
const barWidth = totalBarWidth / Math.max(1, barGroups.length);
|
|
629
|
+
const dash = resolveStrokeDasharray(series, seriesPresentation);
|
|
630
|
+
const areaFillStyle = seriesSvgFillStyle(seriesPresentation);
|
|
471
631
|
return (
|
|
472
632
|
<g
|
|
473
633
|
key={`${series.id}:${value}`}
|
|
474
|
-
className={cx("niceeval-chart-series",
|
|
634
|
+
className={cx("niceeval-chart-series", seriesClassName)}
|
|
475
635
|
data-series={`${series.id}:${value}`}
|
|
476
636
|
>
|
|
477
637
|
{series.mark === "area" && ordered.length > 1 ? (
|
|
478
638
|
<polygon
|
|
479
639
|
className="niceeval-chart-area"
|
|
640
|
+
style={areaFillStyle}
|
|
480
641
|
points={[
|
|
481
642
|
`${ordered[0]!.px},${baseline}`,
|
|
482
643
|
...ordered.map((point) => `${point.px},${point.py}`),
|
|
@@ -488,7 +649,7 @@ function renderChartWeb(
|
|
|
488
649
|
<polyline
|
|
489
650
|
className="niceeval-chart-line"
|
|
490
651
|
points={ordered.map((p) => `${p.px},${p.py}`).join(" ")}
|
|
491
|
-
strokeDasharray={
|
|
652
|
+
strokeDasharray={dash}
|
|
492
653
|
/>
|
|
493
654
|
) : null}
|
|
494
655
|
{ordered.map((p) => {
|
|
@@ -502,6 +663,7 @@ function renderChartWeb(
|
|
|
502
663
|
shape = (
|
|
503
664
|
<rect
|
|
504
665
|
className={cx("niceeval-chart-bar", p.seriesClass)}
|
|
666
|
+
style={seriesSvgFillStyle(p.presentation)}
|
|
505
667
|
x={p.px - totalBarWidth / 2 + groupIndex * barWidth}
|
|
506
668
|
y={Math.min(topY, baseY)}
|
|
507
669
|
width={barWidth}
|
|
@@ -511,10 +673,12 @@ function renderChartWeb(
|
|
|
511
673
|
</rect>
|
|
512
674
|
);
|
|
513
675
|
} else {
|
|
514
|
-
shape = (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
676
|
+
shape = renderMarkerShape(
|
|
677
|
+
p.presentation,
|
|
678
|
+
p.px,
|
|
679
|
+
p.py,
|
|
680
|
+
p.seriesClass,
|
|
681
|
+
<title>{`${p.pointLabel}\n${axes.xField}: ${metricDisplay(p, "x", axes.xMeta, locale)}\n${axes.yField}: ${metricDisplay(p, "y", axes.yMeta, locale)}`}</title>,
|
|
518
682
|
);
|
|
519
683
|
}
|
|
520
684
|
return (
|
|
@@ -798,7 +962,8 @@ export const Chart = defineComponent<ChartProps>({
|
|
|
798
962
|
if (values.length === 0) continue;
|
|
799
963
|
decls[spec.by] = {
|
|
800
964
|
dimension: spec.by,
|
|
801
|
-
|
|
965
|
+
// area 是 FillSeriesPresentation(填充图案),不再折叠成 line。
|
|
966
|
+
encoding: { kind: "series", mark: spec.mark },
|
|
802
967
|
values,
|
|
803
968
|
};
|
|
804
969
|
}
|
|
@@ -870,11 +870,16 @@ function renderCellWeb(
|
|
|
870
870
|
);
|
|
871
871
|
}
|
|
872
872
|
const verdict = cell.verdict ?? "skipped";
|
|
873
|
+
const stale = cell.staleSinceMs !== undefined;
|
|
873
874
|
// 判定符走 verdictMark 单源,与 locator 格同一张表(errored 是 `!`,不并到 `✗`)。
|
|
874
875
|
return (
|
|
875
|
-
<span
|
|
876
|
-
{
|
|
877
|
-
{localeText(ctx.locale,
|
|
876
|
+
<span
|
|
877
|
+
className={cx("niceeval-verdict", `niceeval-verdict-${verdict}`, stale ? "niceeval-stale" : undefined)}
|
|
878
|
+
title={stale ? localeText(ctx.locale, "experimentList.historicalTooltip") : undefined}
|
|
879
|
+
>
|
|
880
|
+
{verdictMark(verdict === "skipped" ? "skipped" : verdict)}
|
|
881
|
+
{!cell.bare ? <>{" "}{localeText(ctx.locale, `verdict.${verdict === "skipped" ? "skipped" : verdict}`)}</> : null}
|
|
882
|
+
{stale ? <span className="niceeval-stale-distance">{formatTimeDistance(cell.staleSinceMs!, ctx.locale)}</span> : null}
|
|
878
883
|
</span>
|
|
879
884
|
);
|
|
880
885
|
}
|
|
@@ -136,8 +136,9 @@ export interface WebContext {
|
|
|
136
136
|
/** chrome 文案的 locale;官方组件渲染面经上下文读取,宿主外默认 "en"。 */
|
|
137
137
|
locale: ReportLocale;
|
|
138
138
|
/**
|
|
139
|
-
* 取本组件 `dimensions()` 声明的某个句柄在这一页的呈现面(
|
|
140
|
-
*
|
|
139
|
+
* 取本组件 `dimensions()` 声明的某个句柄在这一页的呈现面(label / color / series 的
|
|
140
|
+
* fill·stroke·marker 等可直接使用的值)。查别的组件的句柄或没声明的句柄抛
|
|
141
|
+
* `UndeclaredDimensionValueError`。
|
|
141
142
|
*/
|
|
142
143
|
dimension(handle: string): PresentedDimension;
|
|
143
144
|
}
|
|
@@ -203,6 +204,8 @@ export type ReportComponent<P extends object> = ((props: P) => ReactNode) & {
|
|
|
203
204
|
*/
|
|
204
205
|
export interface PageDimensions {
|
|
205
206
|
dimension(props: object, handle: string): PresentedDimension;
|
|
207
|
+
/** 维度 name → 值 → seriesSlot;测试与调试观察槽位分配,组件渲染不读槽号。 */
|
|
208
|
+
readonly slotsByDimension: ReadonlyMap<string, ReadonlyMap<string, number>>;
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
/** 渲染上下文上挂当前页分配结果的内部键;不是契约字段,只在宿主与渲染遍历之间传递。 */
|
|
@@ -331,6 +334,7 @@ export function collectPageDimensions(
|
|
|
331
334
|
|
|
332
335
|
const plan = allocatePageDimensions(handles, pins, { face });
|
|
333
336
|
return {
|
|
337
|
+
slotsByDimension: plan.slotsByDimension,
|
|
334
338
|
dimension(props: object, handle: string): PresentedDimension {
|
|
335
339
|
const entry = byProps.get(props);
|
|
336
340
|
const key = entry?.keys.get(handle);
|
package/src/report/index.ts
CHANGED
|
@@ -69,7 +69,17 @@ export {
|
|
|
69
69
|
toTraceNodes,
|
|
70
70
|
} from "./model/conversions.ts";
|
|
71
71
|
export { presentDimension, shortestUniqueLabels } from "./presentation.ts";
|
|
72
|
-
export type {
|
|
72
|
+
export type {
|
|
73
|
+
ColorPresentation,
|
|
74
|
+
DimensionDeclaration,
|
|
75
|
+
DimensionEncoding,
|
|
76
|
+
DimensionPresentation,
|
|
77
|
+
FillSeriesPresentation,
|
|
78
|
+
LabelPresentation,
|
|
79
|
+
LineSeriesPresentation,
|
|
80
|
+
PresentedDimension,
|
|
81
|
+
ScatterSeriesPresentation,
|
|
82
|
+
} from "./presentation.ts";
|
|
73
83
|
export { flag, label, numericFlag, numericLabel, numericRunConfig, runConfig } from "./model/flag.ts";
|
|
74
84
|
export { evaluationKindComposition } from "./model/evaluation-kind.ts";
|
|
75
85
|
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
NumericAxis,
|
|
20
20
|
ReportInput,
|
|
21
21
|
SeriesInput,
|
|
22
|
+
StaleConclusionReference,
|
|
22
23
|
} from "./types.ts";
|
|
23
24
|
import { flagValueOf, labelValueOf, runConfigValueOf } from "./flag.ts";
|
|
24
25
|
import { metricDisplay } from "./format.ts";
|
|
@@ -92,6 +93,31 @@ export function historicalOf(item: Item): boolean {
|
|
|
92
93
|
return item.attempt.carried || item.run.startedAt < item.watermark.startedAt;
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/** 一个 ISO 时刻距渲染时刻(`Date.now()`)的毫秒数,恒不小于 0。 */
|
|
97
|
+
export function msSince(iso: string): number {
|
|
98
|
+
return Math.max(0, Date.now() - Date.parse(iso));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 覆盖缺口 / 对照矩阵缺席格的「过期结论」参考(docs/feature/reports/components/summaries/
|
|
103
|
+
* experiment-table.md「覆盖缺口的两档占位行」、show/compare.md 同一套口径):候选已经是与
|
|
104
|
+
* 当前基准 configHash 不可比的那些,取其中最近一条;两个消费方(entity-lists 的占位行、
|
|
105
|
+
* DeltaTable 的缺席格)共用同一份「取最新」判据,不各自实现一遍。
|
|
106
|
+
*/
|
|
107
|
+
export function staleReferenceOf(candidates: readonly AttemptHandle[]): StaleConclusionReference | undefined {
|
|
108
|
+
if (candidates.length === 0) return undefined;
|
|
109
|
+
const newest = candidates.reduce((a, b) => ((a.result.startedAt ?? "") >= (b.result.startedAt ?? "") ? a : b));
|
|
110
|
+
const startedAt = newest.result.startedAt;
|
|
111
|
+
if (!startedAt) return undefined; // 无时刻的 legacy 落盘算不出时距,不伪造参考
|
|
112
|
+
return {
|
|
113
|
+
locator:
|
|
114
|
+
newest.locator ??
|
|
115
|
+
encodeAttemptLocator({ runId: newest.run.runId, evalId: newest.evalId, attempt: newest.result.attempt }),
|
|
116
|
+
verdict: newest.result.verdict,
|
|
117
|
+
staleSinceMs: msSince(startedAt),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
/** 快照键:"<experimentId> @ <startedAt>"("run" 维度与手挑快照数组的对比用)。 */
|
|
96
122
|
export function snapshotKeyOf(run: Run): string {
|
|
97
123
|
return `${run.experimentId} @ ${run.startedAt}`;
|
|
@@ -98,7 +98,6 @@ const en = {
|
|
|
98
98
|
"experimentList.cost": "Cost",
|
|
99
99
|
"experimentList.result": "Record",
|
|
100
100
|
"experimentList.status": "Status",
|
|
101
|
-
"experimentList.coverage": "Coverage",
|
|
102
101
|
"experimentList.evalAttempt": "Eval / Attempt",
|
|
103
102
|
"experimentList.duration": "Duration",
|
|
104
103
|
"experimentList.filterPlaceholder": "Filter experiments…",
|
|
@@ -306,7 +305,6 @@ const zhCN: globalThis.Record<ReportMessageKey, string> = {
|
|
|
306
305
|
"experimentList.cost": "成本",
|
|
307
306
|
"experimentList.result": "结果",
|
|
308
307
|
"experimentList.status": "状态",
|
|
309
|
-
"experimentList.coverage": "覆盖构成",
|
|
310
308
|
"experimentList.evalAttempt": "题目 / Attempt",
|
|
311
309
|
"experimentList.duration": "耗时",
|
|
312
310
|
"experimentList.filterPlaceholder": "筛选实验…",
|
|
@@ -275,6 +275,8 @@ export interface DeltaCell {
|
|
|
275
275
|
totalCostUSD?: number;
|
|
276
276
|
/** true 时该格来自跨快照携带的历史执行(时效标注见 experiment-table.md「时效不写字」)。 */
|
|
277
277
|
historical: boolean;
|
|
278
|
+
/** `historical` 为 true 时,距今的毫秒数(渲染时刻起算);新执行时省略,不伪造 0。 */
|
|
279
|
+
staleSinceMs?: number;
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
export interface DeltaData {
|
|
@@ -292,6 +294,12 @@ export interface DeltaData {
|
|
|
292
294
|
cells: globalThis.Record<string, DeltaCell>;
|
|
293
295
|
/** 键是非基准条件值;任一侧缺数据时无键——delta 不把缺失当 0。 */
|
|
294
296
|
delta?: globalThis.Record<string, { score?: number; tokens?: number; costUSD?: number }>;
|
|
297
|
+
/**
|
|
298
|
+
* 键是条件值,只在该条件缺席这道题(`cells` 无键)且记录里存在不可比历史判定时才有条目
|
|
299
|
+
* (docs/feature/reports/show/compare.md「— ✓ 12d」);它不进 `totals`、`delta` 与配对覆盖的
|
|
300
|
+
* 任何一个数,只提供 locator 下钻。
|
|
301
|
+
*/
|
|
302
|
+
references?: globalThis.Record<string, StaleConclusionReference>;
|
|
295
303
|
}>;
|
|
296
304
|
/** 各条件自身覆盖面的描述,分母是该条件有结果的 eval 数;不用于跨条件直接归因。 */
|
|
297
305
|
totals: globalThis.Record<
|