@tangle-network/charts 0.1.0 → 0.1.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/LICENSE +21 -0
- package/README.md +8 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.js +189 -65
- package/package.json +8 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tangle Network
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -15,6 +15,14 @@ The library draws and never decides.
|
|
|
15
15
|
Ranks, intervals, frontier membership, estimate labels and pass counts come from the producer: a board record, a VerticalBench report or an agent-eval lens.
|
|
16
16
|
A figure function returns a `Refusal` in place of a figure it cannot draw honestly, for example a bar over too few units, an interval that names no method, or a frontier the producer did not compute.
|
|
17
17
|
|
|
18
|
+
The producer's `estimate` controls rate marks: `descriptive` draws a rate bar and may show a named Wilson score interval when that estimate meets the producer's unit threshold.
|
|
19
|
+
Task-clustered bootstrap intervals require a `bootstrap` estimate.
|
|
20
|
+
Intervals without support are marked as not drawn in the data table and note.
|
|
21
|
+
A missing, zero, or unproven cost prints `unknown`; zero passes print `no pass`.
|
|
22
|
+
Rank numbers require every row to carry both a rank and a `bootstrap` estimate.
|
|
23
|
+
Malformed rates, intervals, matrix counts, and breakdown counts return a refusal instead of a misleading mark.
|
|
24
|
+
An interval whisker is omitted when its bounds exclude the row's rate.
|
|
25
|
+
|
|
18
26
|
It has no runtime dependencies and uses no DOM, so it runs in a Worker, a build step or a browser.
|
|
19
27
|
|
|
20
28
|
## Use
|
package/dist/index.d.ts
CHANGED
|
@@ -194,12 +194,14 @@ declare function comparisonIntervals(rows: ComparisonRow[], setups: Setup[], o:
|
|
|
194
194
|
* hollow muted circle, and an estimated cost is never a filled disc. An ink
|
|
195
195
|
* line joins frontier points. Every point is labelled directly; a label that
|
|
196
196
|
* collides everywhere becomes a number listed in the table. A setup with no
|
|
197
|
-
* pass
|
|
198
|
-
*
|
|
199
|
-
*
|
|
197
|
+
* pass, an unknown cost or too few units for a rate is listed under the axis,
|
|
198
|
+
* never placed: a log axis has no origin, and a point's height is a rate. On a
|
|
199
|
+
* phone the plot becomes a list in rate order, with the same point marks, a
|
|
200
|
+
* rate bar and the cost as text.
|
|
200
201
|
*
|
|
201
|
-
* Refuses: the whole figure when fewer than two setups have a cost per
|
|
202
|
-
*
|
|
202
|
+
* Refuses: the whole figure when fewer than two setups have both a cost per
|
|
203
|
+
* pass and enough units for a rate; the frontier line unless two or more
|
|
204
|
+
* plotted rows carry `onFront: true` (it never computes dominance itself).
|
|
203
205
|
*/
|
|
204
206
|
|
|
205
207
|
interface CostFrontierOptions {
|
|
@@ -214,14 +216,22 @@ declare const figureCss: string;
|
|
|
214
216
|
/** --chart-* resolved per theme, for documents without brand tokens: light, dark by preference or [data-theme="dark"], light in print. */
|
|
215
217
|
declare const chartTokensCss: string;
|
|
216
218
|
|
|
219
|
+
/**
|
|
220
|
+
* The order rows are drawn in. Ranked rows keep their rank. Otherwise rows
|
|
221
|
+
* with enough units to draw a rate come first, highest rate first; rows with
|
|
222
|
+
* too few units follow, most units first, so a 1/1 row never heads the list.
|
|
223
|
+
*/
|
|
224
|
+
declare function rateOrder<T extends Pick<RateRow, "rank" | "rate" | "estimate" | "attempts">>(rows: readonly T[]): T[];
|
|
225
|
+
|
|
217
226
|
/**
|
|
218
227
|
* `rankedRates`: which setup passes most, and how uncertain is each rate.
|
|
219
228
|
*
|
|
220
229
|
* One row per setup: rank (only when every row is ranked), label, an accent
|
|
221
|
-
* bar on a shared 0–1 axis with
|
|
230
|
+
* bar on a shared 0–1 axis with an eligible interval as an ink whisker, `passed/n`,
|
|
222
231
|
* cost per pass and median run. An App Grade grader adds a tier strip under
|
|
223
|
-
* the bar with a tick at the pass tier.
|
|
224
|
-
* unranked rows sort by rate,
|
|
232
|
+
* the bar with a tick at the pass tier. Bootstrap rows with ranks keep their order;
|
|
233
|
+
* unranked rows sort by rate, rows with too few units last, and the finding
|
|
234
|
+
* says no comparison backs the order.
|
|
225
235
|
*
|
|
226
236
|
* Refuses: a bar when the producer labels the estimate `none` or
|
|
227
237
|
* `insufficient` (the row prints its count only); a whisker whose interval
|
|
@@ -278,4 +288,4 @@ interface TaskMatrixOptions {
|
|
|
278
288
|
}
|
|
279
289
|
declare function taskMatrix(tasks: string[], setupsIn: Setup[], cells: MatrixCell[], o?: TaskMatrixOptions): Figure | Refusal;
|
|
280
290
|
|
|
281
|
-
export { type BreakdownInput, type BreakdownOptions, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type Estimate, type Exclusion, type Figure, type Interval, type IntervalMethod, type MatrixCell, type RankedRatesOptions, type RateRow, type Refusal, type Setup, type TaskMatrixOptions, type Tier, type TierCounts, breakdown, chartTokensCss, comparisonIntervals, costFrontier, figureCss, isRefusal, rankedRates, renderFigure, renderNotes, renderRefusals, taskMatrix };
|
|
291
|
+
export { type BreakdownInput, type BreakdownOptions, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type Estimate, type Exclusion, type Figure, type Interval, type IntervalMethod, type MatrixCell, type RankedRatesOptions, type RateRow, type Refusal, type Setup, type TaskMatrixOptions, type Tier, type TierCounts, breakdown, chartTokensCss, comparisonIntervals, costFrontier, figureCss, isRefusal, rankedRates, rateOrder, renderFigure, renderNotes, renderRefusals, taskMatrix };
|
package/dist/index.js
CHANGED
|
@@ -367,16 +367,22 @@ function breakdown(input, o) {
|
|
|
367
367
|
const id = o.id ?? "breakdown";
|
|
368
368
|
const { reasons, setups } = input;
|
|
369
369
|
if (reasons.length === 0 || setups.length === 0) return { id, refused: `No ${o.measure} to break down.` };
|
|
370
|
+
for (const setup of setups) {
|
|
371
|
+
if (setup.n !== null && (!Number.isInteger(setup.n) || setup.n < 0)) return { id, refused: `${setup.label}: invalid denominator.` };
|
|
372
|
+
}
|
|
370
373
|
const count = /* @__PURE__ */ new Map();
|
|
371
374
|
for (const c of input.counts) {
|
|
372
375
|
if (!reasons.includes(c.reason) || !setups.some((s) => s.id === c.setup)) {
|
|
373
376
|
return { id, refused: `A count names ${c.reason} \xD7 ${c.setup}, which is not in the reason and setup lists.` };
|
|
374
377
|
}
|
|
378
|
+
const key = `${c.reason}\0${c.setup}`;
|
|
379
|
+
if (count.has(key)) return { id, refused: `Two counts for ${c.reason} \xD7 ${c.setup}.` };
|
|
380
|
+
if (c.count !== null && (!Number.isInteger(c.count) || c.count < 0)) return { id, refused: `${c.reason} \xD7 ${c.setup}: invalid count.` };
|
|
375
381
|
const n = setups.find((s) => s.id === c.setup).n;
|
|
376
382
|
if (c.count !== null && n !== null && c.count > n) {
|
|
377
383
|
return { id, refused: `${c.reason} \xD7 ${c.setup}: ${c.count} exceeds the column's ${n} attempts.` };
|
|
378
384
|
}
|
|
379
|
-
count.set(
|
|
385
|
+
count.set(key, c.count);
|
|
380
386
|
}
|
|
381
387
|
const get = (reason, setup) => count.get(`${reason}\0${setup}`) ?? null;
|
|
382
388
|
const withN = setups.filter((s) => s.n !== null);
|
|
@@ -487,6 +493,9 @@ function intervalGap(interval) {
|
|
|
487
493
|
if (!interval) return "no interval";
|
|
488
494
|
const missing = [interval.level === null ? "level" : "", interval.method === null ? "method" : ""].filter(Boolean);
|
|
489
495
|
if (missing.length) return `the interval names no ${missing.join(" or ")}`;
|
|
496
|
+
if (!(interval.method in METHOD_NAME)) return "the interval method is unknown";
|
|
497
|
+
if (!Number.isFinite(interval.level) || interval.level <= 0 || interval.level >= 1) return "the interval level is outside 0\u20131";
|
|
498
|
+
if (!Number.isFinite(interval.lower) || !Number.isFinite(interval.upper)) return "the interval bounds are not finite";
|
|
490
499
|
if (!(interval.lower <= interval.upper)) return "the interval bounds are out of order";
|
|
491
500
|
return null;
|
|
492
501
|
}
|
|
@@ -498,9 +507,47 @@ function estimateNote(estimate, units) {
|
|
|
498
507
|
if (estimate === "insufficient") return "too few units";
|
|
499
508
|
return "";
|
|
500
509
|
}
|
|
510
|
+
function perPass(cost) {
|
|
511
|
+
return cost.basis !== "unknown" && cost.perSolvedUsd !== null && Number.isFinite(cost.perSolvedUsd) && cost.perSolvedUsd > 0 ? cost.perSolvedUsd : null;
|
|
512
|
+
}
|
|
501
513
|
function costText(cost, solved) {
|
|
502
|
-
if (
|
|
503
|
-
|
|
514
|
+
if (solved === 0) return "no pass";
|
|
515
|
+
const value = perPass(cost);
|
|
516
|
+
if (value === null) return "unknown";
|
|
517
|
+
return cost.basis === "receipts" ? usd(value) : `${usd(value)} est.`;
|
|
518
|
+
}
|
|
519
|
+
function rateIntervalGap(row) {
|
|
520
|
+
if (!drawable(row.estimate)) return `${row.estimate} estimate`;
|
|
521
|
+
if (!Number.isFinite(row.rate) || row.rate < 0 || row.rate > 1) return "rate is outside 0\u20131";
|
|
522
|
+
const gap = intervalGap(row.interval);
|
|
523
|
+
if (gap) return gap;
|
|
524
|
+
if (row.interval.method === "mcnemar-exact") return "exact McNemar applies to paired comparisons, not rates";
|
|
525
|
+
if (row.estimate === "descriptive" && row.interval.method !== "wilson") {
|
|
526
|
+
return `descriptive estimate does not support ${intervalName(row.interval)}`;
|
|
527
|
+
}
|
|
528
|
+
if (row.interval.lower < 0 || row.interval.upper > 1) return "the rate interval is outside 0\u20131";
|
|
529
|
+
if (row.rate < row.interval.lower - 1e-6 || row.rate > row.interval.upper + 1e-6) {
|
|
530
|
+
return "the rate falls outside its interval";
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
function rateDataGap(row) {
|
|
535
|
+
if (!Number.isInteger(row.attempts) || row.attempts < 0 || !Number.isInteger(row.solved) || row.solved < 0 || row.solved > row.attempts) {
|
|
536
|
+
return "passed and attempt counts are invalid";
|
|
537
|
+
}
|
|
538
|
+
if (!Number.isFinite(row.rate) || row.rate < 0 || row.rate > 1) return "rate is outside 0\u20131";
|
|
539
|
+
if (row.attempts > 0 && Math.abs(row.rate - row.solved / row.attempts) > 1e-6) return "rate disagrees with passed/attempts";
|
|
540
|
+
if (row.attempts === 0 && row.rate !== 0) return "rate has no attempts";
|
|
541
|
+
if (drawable(row.estimate) && row.attempts === 0) return "drawable estimate has no attempts";
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
function rateOrder(rows) {
|
|
545
|
+
if (rows.length > 0 && rows.every((row) => row.rank !== null && row.estimate === "bootstrap")) {
|
|
546
|
+
return [...rows].sort((a, b) => a.rank - b.rank);
|
|
547
|
+
}
|
|
548
|
+
const drawn = rows.filter((row) => drawable(row.estimate)).sort((a, b) => b.rate - a.rate);
|
|
549
|
+
const counted = rows.filter((row) => !drawable(row.estimate)).sort((a, b) => b.attempts - a.attempts);
|
|
550
|
+
return [...drawn, ...counted];
|
|
504
551
|
}
|
|
505
552
|
function tierSummary(tiers, unmeasured = 0, flagged = 0) {
|
|
506
553
|
const parts = tiers ? TIERS_BEST_FIRST.filter((t) => tiers[t] > 0).map((t) => `${tiers[t]}${t}`) : [];
|
|
@@ -575,8 +622,14 @@ function gaps(row) {
|
|
|
575
622
|
const out = [];
|
|
576
623
|
const gap = intervalGap(row.interval);
|
|
577
624
|
if (gap) out.push(gap);
|
|
625
|
+
if (row.exactInterval !== null) {
|
|
626
|
+
const exactGap = intervalGap(row.exactInterval);
|
|
627
|
+
if (exactGap) out.push(`exact interval: ${exactGap}`);
|
|
628
|
+
}
|
|
578
629
|
if (row.pairs === null) out.push("no pair count");
|
|
630
|
+
else if (!Number.isInteger(row.pairs) || row.pairs <= 0) out.push("invalid pair count");
|
|
579
631
|
if (row.minimumEffect === null) out.push("no registered minimum effect");
|
|
632
|
+
else if (!Number.isFinite(row.minimumEffect)) out.push("invalid registered minimum effect");
|
|
580
633
|
return out;
|
|
581
634
|
}
|
|
582
635
|
function comparisonIntervals(rows, setups, o) {
|
|
@@ -594,7 +647,7 @@ function comparisonIntervals(rows, setups, o) {
|
|
|
594
647
|
if (drawn.length === 0) {
|
|
595
648
|
return {
|
|
596
649
|
id,
|
|
597
|
-
refused: rows.every((r) => r.interval === null) ? "
|
|
650
|
+
refused: rows.every((r) => r.interval === null) ? `${rows.length} ${plural(rows.length, "comparison")} listed; none carries an interval.` : `No comparison can be drawn. ${refused.join("; ")}.`
|
|
598
651
|
};
|
|
599
652
|
}
|
|
600
653
|
const clears = drawn.filter(
|
|
@@ -615,20 +668,20 @@ function comparisonIntervals(rows, setups, o) {
|
|
|
615
668
|
{ label: "Interval", numeric: true },
|
|
616
669
|
{ label: "Exact interval", numeric: true }
|
|
617
670
|
];
|
|
618
|
-
const fmt = (i) => i && intervalGap(i) === null ? `${pts(i.lower)} to ${pts(i.upper)} (${intervalName(i)})` : i ?
|
|
671
|
+
const fmt = (i, n) => i && intervalGap(i) === null ? `${pts(i.lower)} to ${pts(i.upper)} (${intervalName(i)}, n=${n})` : i ? `not drawn (${intervalGap(i)})` : "none";
|
|
619
672
|
const tableRows = rows.map((row) => [
|
|
620
673
|
title(row),
|
|
621
674
|
row.pairs === null ? "not recorded" : String(row.pairs),
|
|
622
675
|
row.minimumEffect === null ? "none registered" : pts(row.minimumEffect),
|
|
623
|
-
fmt(row.interval),
|
|
624
|
-
fmt(row.exactInterval)
|
|
676
|
+
fmt(row.interval, row.pairs),
|
|
677
|
+
fmt(row.exactInterval, row.pairs)
|
|
625
678
|
]);
|
|
626
679
|
const values = drawn.flatMap(({ row }) => [
|
|
627
680
|
row.interval.lower,
|
|
628
681
|
row.interval.upper,
|
|
629
682
|
row.minimumEffect,
|
|
630
683
|
0,
|
|
631
|
-
...row.exactInterval ? [row.exactInterval.lower, row.exactInterval.upper] : []
|
|
684
|
+
...intervalGap(row.exactInterval) === null ? [row.exactInterval.lower, row.exactInterval.upper] : []
|
|
632
685
|
]);
|
|
633
686
|
const span = Math.max(...values) - Math.min(...values) || 0.1;
|
|
634
687
|
const domain = [Math.min(...values) - span * 0.08, Math.max(...values) + span * 0.08];
|
|
@@ -678,7 +731,7 @@ function draw(drawn, domain, width, plotX, labelW) {
|
|
|
678
731
|
const me = x(row.minimumEffect);
|
|
679
732
|
marks += line(me, barY - 8, me, barY + 20, "tgc-threshold");
|
|
680
733
|
body += titled(
|
|
681
|
-
`${title} \xB7 ${row.pairs} pairs \xB7 ${pts(row.interval.lower)} to ${pts(row.interval.upper)} \xB7 minimum effect ${pts(row.minimumEffect)}`,
|
|
734
|
+
`${title} \xB7 ${row.pairs} pairs \xB7 ${intervalName(row.interval)} interval ${pts(row.interval.lower)} to ${pts(row.interval.upper)}${rateExactTitle(row)} \xB7 minimum effect ${pts(row.minimumEffect)}`,
|
|
682
735
|
marks
|
|
683
736
|
);
|
|
684
737
|
}
|
|
@@ -689,57 +742,94 @@ function draw(drawn, domain, width, plotX, labelW) {
|
|
|
689
742
|
}
|
|
690
743
|
return svgRoot(width, y + 24, axis + body);
|
|
691
744
|
}
|
|
745
|
+
function rateExactTitle(row) {
|
|
746
|
+
return intervalGap(row.exactInterval) === null ? ` \xB7 ${intervalName(row.exactInterval)} exact interval ${pts(row.exactInterval.lower)} to ${pts(row.exactInterval.upper)}` : "";
|
|
747
|
+
}
|
|
692
748
|
|
|
693
749
|
// src/cost-frontier.ts
|
|
694
750
|
var overlaps = (a, b) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
|
|
751
|
+
function crosses(box, a, b) {
|
|
752
|
+
const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
|
|
753
|
+
let t0 = 0;
|
|
754
|
+
let t1 = 1;
|
|
755
|
+
for (const [p, q] of [
|
|
756
|
+
[-dx, a[0] - box.x0],
|
|
757
|
+
[dx, box.x1 - a[0]],
|
|
758
|
+
[-dy, a[1] - box.y0],
|
|
759
|
+
[dy, box.y1 - a[1]]
|
|
760
|
+
]) {
|
|
761
|
+
if (p === 0) {
|
|
762
|
+
if (q < 0) return false;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
const t = q / p;
|
|
766
|
+
if (p < 0) t0 = Math.max(t0, t);
|
|
767
|
+
else t1 = Math.min(t1, t);
|
|
768
|
+
if (t0 > t1) return false;
|
|
769
|
+
}
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
var plottable = (row) => row.solved > 0 && perPass(row.cost) !== null && drawable(row.estimate);
|
|
773
|
+
function unplacedReason(row) {
|
|
774
|
+
if (row.solved === 0) return "0 passed";
|
|
775
|
+
if (!drawable(row.estimate)) return estimateNote(row.estimate, row.attempts);
|
|
776
|
+
return "cost unknown";
|
|
777
|
+
}
|
|
695
778
|
function costFrontier(rows, o) {
|
|
696
779
|
const id = o.id ?? "cost-frontier";
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
780
|
+
for (const row of rows) {
|
|
781
|
+
const gap = rateDataGap(row);
|
|
782
|
+
if (gap) return { id, refused: `${row.label}: ${gap}.` };
|
|
783
|
+
}
|
|
784
|
+
const costed = rows.filter(plottable);
|
|
785
|
+
const unplaced = rateOrder(rows.filter((row) => !plottable(row)));
|
|
786
|
+
if (!rows.some((row) => row.solved > 0 && perPass(row.cost) !== null)) return { id, refused: `No setup has a cost per pass, so cost is not drawn.` };
|
|
700
787
|
if (costed.length < 2) {
|
|
701
788
|
return {
|
|
702
789
|
id,
|
|
703
|
-
refused: `Fewer than two setups have a cost per pass (${costed.length} of ${rows.length}); ${unplacedText(unplaced)}.`
|
|
790
|
+
refused: `Fewer than two setups have both a cost per pass and enough units for a rate (${costed.length} of ${rows.length}); ${unplacedText(unplaced)}.`
|
|
704
791
|
};
|
|
705
792
|
}
|
|
706
793
|
const front = costed.filter((row) => row.onFront === true);
|
|
707
794
|
const drawLine = front.length >= 2;
|
|
708
|
-
const
|
|
709
|
-
const
|
|
795
|
+
const cost = (row) => perPass(row.cost);
|
|
796
|
+
const cheapestFront = [...front].sort((a, b) => cost(a) - cost(b))[0];
|
|
797
|
+
const top = rateOrder(rows)[0];
|
|
710
798
|
const est = (row) => row.cost.basis === "receipts" ? "" : " (estimated)";
|
|
711
799
|
let finding;
|
|
712
|
-
if (cheapestFront && top
|
|
713
|
-
const saving = 1 - cheapestFront
|
|
714
|
-
finding = `${cheapestFront.label} costs ${usd(cheapestFront
|
|
800
|
+
if (cheapestFront && costed.includes(top) && cheapestFront.id !== top.id) {
|
|
801
|
+
const saving = 1 - cost(cheapestFront) / cost(top);
|
|
802
|
+
finding = `${cheapestFront.label} costs ${usd(cost(cheapestFront))} per pass${est(cheapestFront)}, ${pct(saving)} less than ${top.label}.`;
|
|
715
803
|
} else if (cheapestFront && cheapestFront.id === top.id) {
|
|
716
804
|
finding = `${top.label} leads on both ${o.measure} and cost per pass.`;
|
|
717
805
|
} else {
|
|
718
|
-
const cheapest = [...costed].sort((a, b) => a
|
|
719
|
-
finding = `${cheapest.label} has the lowest cost per pass, ${usd(cheapest
|
|
806
|
+
const cheapest = [...costed].sort((a, b) => cost(a) - cost(b))[0];
|
|
807
|
+
finding = `${cheapest.label} has the lowest cost per pass, ${usd(cost(cheapest))}${est(cheapest)}.`;
|
|
720
808
|
}
|
|
721
|
-
const lede = `${costed.length} of ${rows.length} setups
|
|
722
|
-
const read = `
|
|
723
|
-
const intervals = [...new Set(costed.filter((r) =>
|
|
809
|
+
const lede = `${costed.length} of ${rows.length} setups are plotted; ` + (drawLine ? `${front.length} sit on the producer's frontier.` : front.length === 1 ? "1 sits on the producer's frontier." : "the producer computed no frontier.");
|
|
810
|
+
const read = `Higher ${o.measure} and lower cost per pass are better; on the wide plot, better is up and right.` + (drawLine ? " Filled points sit on the frontier; the line joins them." : "") + (costed.some((r) => r.onFront && r.cost.basis !== "receipts") ? " A hollow accent ring is a frontier point with an estimated cost." : "");
|
|
811
|
+
const intervals = [...new Set(costed.filter((r) => rateIntervalGap(r) === null).map((r) => intervalName(r.interval)))];
|
|
724
812
|
const bases = [...new Set(costed.map((r) => r.cost.basis))];
|
|
725
813
|
const method = [
|
|
726
814
|
`Cost per pass is model spend over passed attempts, ${bases.map((b) => b === "receipts" ? "from router receipts" : b === "estimated" ? "estimated, not billed" : "from an unrecorded source").join(" or ")}.`,
|
|
727
|
-
"The frontier is the producer's: no setup off it is both cheaper and higher.",
|
|
815
|
+
front.length ? "The frontier is the producer's: no setup off it is both cheaper and higher." : "The producer did not compute a frontier.",
|
|
728
816
|
intervals.length ? `Vertical lines: ${intervals.join(", ")} on the ${o.measure}.` : ""
|
|
729
817
|
].filter(Boolean).join(" ");
|
|
730
818
|
const columns = [
|
|
731
819
|
{ label: "Setup" },
|
|
732
820
|
{ label: capitalize(o.measure), numeric: true },
|
|
733
821
|
{ label: "Passed", numeric: true },
|
|
822
|
+
{ label: "Interval" },
|
|
734
823
|
{ label: "Cost per pass", numeric: true },
|
|
735
824
|
{ label: "Cost basis" },
|
|
736
825
|
{ label: "On frontier" }
|
|
737
826
|
];
|
|
738
|
-
const sorted =
|
|
827
|
+
const sorted = rateOrder(rows);
|
|
739
828
|
const tableRows = sorted.map((row) => [
|
|
740
829
|
row.label,
|
|
741
|
-
pct(row.rate),
|
|
830
|
+
drawable(row.estimate) ? pct(row.rate) : estimateNote(row.estimate, row.attempts),
|
|
742
831
|
kn(row.solved, row.attempts),
|
|
832
|
+
rateIntervalGap(row) === null ? `${pct(row.interval.lower)}\u2013${pct(row.interval.upper)} (${intervalName(row.interval)}, n=${row.attempts})` : `not drawn (${rateIntervalGap(row)})`,
|
|
743
833
|
costText(row.cost, row.solved),
|
|
744
834
|
row.cost.basis,
|
|
745
835
|
row.onFront === null ? "not computed" : row.onFront ? "yes" : "no"
|
|
@@ -760,7 +850,10 @@ function costFrontier(rows, o) {
|
|
|
760
850
|
note: {
|
|
761
851
|
method,
|
|
762
852
|
n: `${rows.reduce((s, r) => s + r.attempts, 0)} attempts over ${rows.length} setups.`,
|
|
763
|
-
exclusions:
|
|
853
|
+
exclusions: [
|
|
854
|
+
...unplaced.length ? [`Not placed: ${unplacedText(unplaced)}.`] : [],
|
|
855
|
+
...costed.filter((r) => rateIntervalGap(r) !== null).map((r) => `No interval for ${r.label}: ${rateIntervalGap(r)}.`)
|
|
856
|
+
]
|
|
764
857
|
}
|
|
765
858
|
};
|
|
766
859
|
}
|
|
@@ -769,7 +862,7 @@ function narrowList(rows) {
|
|
|
769
862
|
let out = "";
|
|
770
863
|
let y = 4;
|
|
771
864
|
for (const [i, row] of rows.entries()) {
|
|
772
|
-
const placed = row
|
|
865
|
+
const placed = plottable(row);
|
|
773
866
|
const label = wrap(row.label, NARROW - 16, 14);
|
|
774
867
|
let marks = placed ? circle(4, y + 10, 4, row.onFront ? row.cost.basis === "receipts" ? "tgc-pt-front" : "tgc-pt-est" : "tgc-pt") : "";
|
|
775
868
|
marks += textBlock(label.lines, 16, y + 14, 18, `class="tgc-ink" font-size="14"`);
|
|
@@ -777,7 +870,7 @@ function narrowList(rows) {
|
|
|
777
870
|
if (drawable(row.estimate)) {
|
|
778
871
|
let end = row.rate * x1;
|
|
779
872
|
marks += hbar(0, cy, end, 10, "tgc-bar");
|
|
780
|
-
if (
|
|
873
|
+
if (rateIntervalGap(row) === null) {
|
|
781
874
|
const lo = row.interval.lower * x1;
|
|
782
875
|
const hi = row.interval.upper * x1;
|
|
783
876
|
marks += line(lo, cy + 5, hi, cy + 5, "tgc-whisker") + line(lo, cy + 1, lo, cy + 9, "tgc-whisker") + line(hi, cy + 1, hi, cy + 9, "tgc-whisker");
|
|
@@ -787,10 +880,12 @@ function narrowList(rows) {
|
|
|
787
880
|
cy += 16;
|
|
788
881
|
}
|
|
789
882
|
const count = `${kn(row.solved, row.attempts)} passed${drawable(row.estimate) ? "" : `, ${estimateNote(row.estimate, row.attempts)}`}`;
|
|
790
|
-
const
|
|
883
|
+
const reason = row.solved > 0 && drawable(row.estimate) ? "cost unknown \xB7 " : "";
|
|
884
|
+
const fact = placed ? `${count} \xB7 ${costText(row.cost, row.solved)} per pass${row.onFront ? " \xB7 on the frontier" : ""}` : `${count} \xB7 ${reason}not plotted`;
|
|
791
885
|
const facts = fitLines(fact.split(" \xB7 "), NARROW, 12, "mono");
|
|
792
886
|
marks += textBlock(facts, 0, cy + 12, 16, `class="tgc-ink-muted tgc-num" font-size="12"`);
|
|
793
|
-
|
|
887
|
+
const interval = rateIntervalGap(row) === null ? ` \xB7 ${intervalName(row.interval)} interval ${pct(row.interval.lower)}\u2013${pct(row.interval.upper)}, n=${row.attempts}` : "";
|
|
888
|
+
out += titled(`${row.label} \xB7 ${fact}${interval}`, marks);
|
|
794
889
|
y = cy + 4 + facts.length * 16;
|
|
795
890
|
if (i < rows.length - 1) out += line(0, y + 2, NARROW, y + 2, "tgc-rule");
|
|
796
891
|
y += 10;
|
|
@@ -804,19 +899,25 @@ function narrowList(rows) {
|
|
|
804
899
|
return svgRoot(NARROW, y, out);
|
|
805
900
|
}
|
|
806
901
|
function unplacedText(rows) {
|
|
807
|
-
return listJoin(rows.map((r) => `${r.label}
|
|
902
|
+
return listJoin(rows.map((r) => `${r.label} (${unplacedReason(r)})`));
|
|
808
903
|
}
|
|
809
904
|
function plot(costed, unplaced, drawLine, o) {
|
|
810
905
|
const left = 48;
|
|
811
906
|
const right = WIDE - 24;
|
|
812
907
|
const top = 32;
|
|
813
908
|
const bottom = 272;
|
|
814
|
-
const costs = costed.map((r) => r.cost
|
|
909
|
+
const costs = costed.map((r) => perPass(r.cost));
|
|
815
910
|
const [lo, hi] = logDomain(Math.min(...costs), Math.max(...costs));
|
|
816
911
|
const x = log([lo, hi], [left, right].reverse());
|
|
817
|
-
const
|
|
912
|
+
const highest = Math.max(
|
|
913
|
+
...costed.map((r) => Math.max(r.rate, rateIntervalGap(r) === null ? r.interval.upper : 0))
|
|
914
|
+
);
|
|
915
|
+
const yMax = Math.min(1, Math.max(0.2, Math.ceil(highest * 5 - 1e-9) / 5));
|
|
916
|
+
const yStep = yMax <= 0.4 ? 0.1 : 0.2;
|
|
917
|
+
const y = (v) => bottom - v / yMax * (bottom - top);
|
|
818
918
|
let out = text(0, 14, capitalize(o.measure), { cls: "tgc-ink-muted" });
|
|
819
|
-
for (
|
|
919
|
+
for (let i = 0; i <= Math.round(yMax / yStep); i++) {
|
|
920
|
+
const v = Number((i * yStep).toFixed(10));
|
|
820
921
|
out += line(left, y(v), right, y(v), v === 0 ? "tgc-axis" : "tgc-gridline");
|
|
821
922
|
out += text(left - 8, y(v) + 4, pct(v), { cls: "tgc-ink-muted tgc-num", anchor: "end" });
|
|
822
923
|
}
|
|
@@ -830,22 +931,20 @@ function plot(costed, unplaced, drawLine, o) {
|
|
|
830
931
|
});
|
|
831
932
|
const placed = costed.map((row) => ({
|
|
832
933
|
row,
|
|
833
|
-
x: x(row.cost
|
|
934
|
+
x: x(perPass(row.cost)),
|
|
834
935
|
y: y(row.rate),
|
|
835
936
|
estimated: row.cost.basis !== "receipts"
|
|
836
937
|
}));
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
out += polyline(pts2.map((p) => [p.x, p.y]), "tgc-front-line");
|
|
840
|
-
}
|
|
938
|
+
const frontLine = drawLine ? placed.filter((p) => p.row.onFront).sort((a, b) => a.x - b.x).map((p) => [p.x, p.y]) : [];
|
|
939
|
+
if (drawLine) out += polyline(frontLine, "tgc-front-line");
|
|
841
940
|
for (const p of placed) {
|
|
842
|
-
if (
|
|
941
|
+
if (rateIntervalGap(p.row) === null) {
|
|
843
942
|
out += line(p.x, y(p.row.interval.lower), p.x, y(p.row.interval.upper), "tgc-pt-ci");
|
|
844
943
|
}
|
|
845
944
|
}
|
|
846
945
|
const obstacles = placed.map((p) => ({ x0: p.x - 6, y0: p.y - 6, x1: p.x + 6, y1: p.y + 6 }));
|
|
847
946
|
for (const p of placed) {
|
|
848
|
-
if (
|
|
947
|
+
if (rateIntervalGap(p.row) === null) {
|
|
849
948
|
obstacles.push({ x0: p.x - 2, y0: y(p.row.interval.upper), x1: p.x + 2, y1: y(p.row.interval.lower) });
|
|
850
949
|
}
|
|
851
950
|
}
|
|
@@ -869,7 +968,8 @@ function plot(costed, unplaced, drawLine, o) {
|
|
|
869
968
|
const x0 = anchor === "start" ? lx : anchor === "end" ? lx - w : lx - w / 2;
|
|
870
969
|
const box = { x0, y0: ly - 11, x1: x0 + w, y1: ly + 3 };
|
|
871
970
|
const inside = box.x0 >= bounds.x0 && box.x1 <= bounds.x1 && box.y0 >= bounds.y0 && box.y1 <= bounds.y1;
|
|
872
|
-
|
|
971
|
+
const onLine = frontLine.some((a, i) => i > 0 && crosses(box, frontLine[i - 1], a));
|
|
972
|
+
if (!inside || onLine || obstacles.some((b) => overlaps(b, box))) continue;
|
|
873
973
|
obstacles.push(box);
|
|
874
974
|
if (leader) labels.push(line(p.x, p.y, anchor === "start" ? lx - 3 : lx + 3, ly - 4, "tgc-leader"));
|
|
875
975
|
labels.push(text(lx, ly, p.row.label, { cls: "tgc-ink", anchor }));
|
|
@@ -885,7 +985,7 @@ function plot(costed, unplaced, drawLine, o) {
|
|
|
885
985
|
for (const p of placed) {
|
|
886
986
|
const cls = p.row.onFront ? p.estimated ? "tgc-pt-est" : "tgc-pt-front" : "tgc-pt";
|
|
887
987
|
out += titled(
|
|
888
|
-
`${p.row.label} \xB7 ${pct(p.row.rate)} ${o.measure} \xB7 ${costText(p.row.cost, p.row.solved)} per pass${p.row.onFront ? " \xB7 on the frontier" : ""}`,
|
|
988
|
+
`${p.row.label} \xB7 ${pct(p.row.rate)} ${o.measure} \xB7 ${kn(p.row.solved, p.row.attempts)} passed \xB7 ${costText(p.row.cost, p.row.solved)} per pass${rateIntervalGap(p.row) === null ? ` \xB7 ${intervalName(p.row.interval)} interval ${pct(p.row.interval.lower)}\u2013${pct(p.row.interval.upper)}, n=${p.row.attempts}` : ""}${p.row.onFront ? " \xB7 on the frontier" : ""}`,
|
|
889
989
|
circle(p.x, p.y, 4, cls)
|
|
890
990
|
);
|
|
891
991
|
}
|
|
@@ -1207,36 +1307,44 @@ function rankedRates(input, o) {
|
|
|
1207
1307
|
const noun = o.nouns ?? { attempt: "attempt", setup: "setup" };
|
|
1208
1308
|
if (input.length === 0) return { id, refused: `No ${noun.setup} has an ${noun.attempt}.` };
|
|
1209
1309
|
for (const row of input) {
|
|
1210
|
-
const
|
|
1211
|
-
if (
|
|
1212
|
-
|
|
1310
|
+
const gap = rateDataGap(row);
|
|
1311
|
+
if (gap) return { id, refused: `${row.label}: ${gap}.` };
|
|
1312
|
+
if (row.tiers) {
|
|
1313
|
+
if (TIERS_BEST_FIRST.some((tier) => !Number.isInteger(row.tiers[tier]) || row.tiers[tier] < 0)) {
|
|
1314
|
+
return { id, refused: `${row.label}: tier counts must be nonnegative whole numbers.` };
|
|
1315
|
+
}
|
|
1316
|
+
if (tierTotal(row.tiers) > row.attempts) return { id, refused: `${row.label}: tier counts exceed attempts.` };
|
|
1317
|
+
if (o.passTier) {
|
|
1318
|
+
const passing = TIERS_BEST_FIRST.filter((tier) => tierAtLeast(tier, o.passTier)).reduce((sum, tier) => sum + row.tiers[tier], 0);
|
|
1319
|
+
if (passing !== row.solved) return { id, refused: `${row.label}: ${row.solved} passes disagree with ${passing} grades at ${o.passTier} or better.` };
|
|
1320
|
+
}
|
|
1213
1321
|
}
|
|
1214
1322
|
}
|
|
1215
|
-
const ranked = input.every((row) => row.rank !== null);
|
|
1216
|
-
const rows =
|
|
1323
|
+
const ranked = input.every((row) => row.rank !== null && row.estimate === "bootstrap");
|
|
1324
|
+
const rows = rateOrder(input);
|
|
1217
1325
|
const layout = {
|
|
1218
1326
|
ranked,
|
|
1219
|
-
cost: rows.some((r) => r.cost
|
|
1327
|
+
cost: rows.some((r) => perPass(r.cost) !== null || r.cost.basis !== "unknown" || r.cost.receipts !== null),
|
|
1220
1328
|
median: rows.some((r) => r.medianWallMs !== null)
|
|
1221
1329
|
};
|
|
1222
1330
|
const bars = rows.filter((row) => drawable(row.estimate));
|
|
1223
1331
|
const noBar = rows.filter((row) => !drawable(row.estimate));
|
|
1224
|
-
const whiskers = bars.filter((row) =>
|
|
1225
|
-
const noWhisker = bars.filter((row) =>
|
|
1332
|
+
const whiskers = bars.filter((row) => rateIntervalGap(row) === null);
|
|
1333
|
+
const noWhisker = bars.filter((row) => rateIntervalGap(row) !== null);
|
|
1226
1334
|
const tiered = rows.some((row) => row.tiers !== null);
|
|
1227
1335
|
const passed = rows.reduce((sum, row) => sum + row.solved, 0);
|
|
1228
1336
|
const attempts = rows.reduce((sum, row) => sum + row.attempts, 0);
|
|
1229
1337
|
const setupsN = `${rows.length} ${plural(rows.length, noun.setup)}`;
|
|
1230
1338
|
const top = rows[0];
|
|
1231
|
-
const uniqueTop =
|
|
1232
|
-
const finding = ranked ? `${top.label} ranks first, passing ${pct(top.rate)} of ${plural(2, noun.attempt)}.` :
|
|
1233
|
-
const lede = `${passed} of ${attempts} ${plural(attempts, noun.attempt)} passed across ${setupsN}.` + (noBar.length
|
|
1339
|
+
const uniqueTop = bars.length === 1 || bars.length > 1 && bars[1].rate < top.rate;
|
|
1340
|
+
const finding = ranked ? `${top.label} ranks first, passing ${pct(top.rate)} of ${plural(2, noun.attempt)}.` : bars.length === 0 ? `No ${noun.setup} has enough units to draw a ${o.measure}.` : rows.length === 1 ? `${top.label} passes ${pct(top.rate)} of ${top.attempts} ${plural(top.attempts, noun.attempt)}.` : uniqueTop ? `${top.label} has the highest ${o.measure}, ${pct(top.rate)} of ${top.attempts} ${plural(top.attempts, noun.attempt)}; no comparison backs the order.` : `Sorted by ${o.measure}; no comparison backs the order.`;
|
|
1341
|
+
const lede = `${passed} of ${attempts} ${plural(attempts, noun.attempt)} passed across ${setupsN}.` + (noBar.length && bars.length ? ` ${noBar.length} of ${setupsN} have too few units to draw a rate; they come last.` : "");
|
|
1234
1342
|
const intervalNames = [...new Set(whiskers.map((row) => intervalName(row.interval)))];
|
|
1235
1343
|
const read = bars.length ? [
|
|
1236
1344
|
`Bars show the share of ${plural(2, noun.attempt)} that passed${whiskers.length ? `; whiskers show the ${intervalNames.join(" or ")} interval` : ""}.`,
|
|
1237
1345
|
tiered ? `The strip under a bar splits ${plural(2, noun.attempt)} by App Grade tier${o.passTier ? `, and the tick marks ${o.passTier} or better` : ""}.` : ""
|
|
1238
1346
|
].filter(Boolean).join(" ") : `Counts show ${plural(2, noun.attempt)} passed; no ${noun.setup} has enough units to draw a rate.`;
|
|
1239
|
-
const bases = [...new Set(rows.filter((r) => r.cost
|
|
1347
|
+
const bases = [...new Set(rows.filter((r) => perPass(r.cost) !== null).map((r) => r.cost.basis))];
|
|
1240
1348
|
const method = [
|
|
1241
1349
|
`${capitalize(o.measure)} is the share of ${plural(2, noun.attempt)} that passed${o.passTier ? `; a pass is App Grade ${o.passTier} or better` : ""}.`,
|
|
1242
1350
|
intervalNames.length ? `Intervals: ${intervalNames.join(", ")}, per ${noun.setup}.` : "",
|
|
@@ -1246,7 +1354,7 @@ function rankedRates(input, o) {
|
|
|
1246
1354
|
const exclusions = [
|
|
1247
1355
|
...(o.excluded ?? []).map((e) => `${e.count} ${plural(e.count, noun.attempt)} excluded: ${e.why}.`),
|
|
1248
1356
|
...noBar.length ? [`No bar for ${noBar.map((r) => `${r.label} (${estimateNote(r.estimate, r.attempts)})`).join(", ")}.`] : [],
|
|
1249
|
-
...noWhisker.length ? [`No whisker for ${noWhisker.map((r) => `${r.label} (${
|
|
1357
|
+
...noWhisker.length ? [`No whisker for ${noWhisker.map((r) => `${r.label} (${rateIntervalGap(r)})`).join(", ")}.`] : []
|
|
1250
1358
|
];
|
|
1251
1359
|
const columns = [
|
|
1252
1360
|
{ label: capitalize(noun.setup) },
|
|
@@ -1267,7 +1375,7 @@ function rankedRates(input, o) {
|
|
|
1267
1375
|
String(row.solved),
|
|
1268
1376
|
String(row.attempts),
|
|
1269
1377
|
drawable(row.estimate) ? pct(row.rate) : estimateNote(row.estimate, row.attempts),
|
|
1270
|
-
row
|
|
1378
|
+
rateIntervalGap(row) === null ? `${pct(row.interval.lower)}\u2013${pct(row.interval.upper)} (${intervalName(row.interval)}, n=${row.attempts})` : `not drawn (${rateIntervalGap(row)})`,
|
|
1271
1379
|
...tiered ? [row.tiers ? [tierSummary(row.tiers), offLadder ? `${offLadder} off the ladder` : ""].filter(Boolean).join(" \xB7 ") || "none" : "not graded"] : [],
|
|
1272
1380
|
...layout.cost ? [costText(row.cost, row.solved), row.cost.basis + (row.cost.receipts !== null ? `, ${row.cost.receipts} receipts` : "")] : [],
|
|
1273
1381
|
...layout.median ? [row.medianWallMs === null ? "unknown" : duration(row.medianWallMs)] : []
|
|
@@ -1297,7 +1405,7 @@ function barMarks(row, x0, x1, y, o) {
|
|
|
1297
1405
|
const at = (v) => x0 + Math.max(0, Math.min(1, v)) * w;
|
|
1298
1406
|
let out = hbar(x0, y, at(row.rate) - x0, 12, "tgc-bar");
|
|
1299
1407
|
let end = at(row.rate);
|
|
1300
|
-
if (row.interval &&
|
|
1408
|
+
if (row.interval && rateIntervalGap(row) === null) {
|
|
1301
1409
|
const lo = at(row.interval.lower);
|
|
1302
1410
|
const hi = at(row.interval.upper);
|
|
1303
1411
|
out += line(lo, y + 6, hi, y + 6, "tgc-whisker");
|
|
@@ -1329,11 +1437,11 @@ function hoverText(row) {
|
|
|
1329
1437
|
`${kn(row.solved, row.attempts)} passed`,
|
|
1330
1438
|
drawable(row.estimate) ? pct(row.rate) : estimateNote(row.estimate, row.attempts)
|
|
1331
1439
|
];
|
|
1332
|
-
if (row.interval &&
|
|
1440
|
+
if (row.interval && rateIntervalGap(row) === null) {
|
|
1333
1441
|
parts.push(`${intervalName(row.interval)} ${pct(row.interval.lower)}\u2013${pct(row.interval.upper)}`);
|
|
1334
1442
|
}
|
|
1335
1443
|
if (row.tiers) parts.push(tierSummary(row.tiers));
|
|
1336
|
-
if (row.cost
|
|
1444
|
+
if (perPass(row.cost) !== null) parts.push(`${costText(row.cost, row.solved)} per pass`);
|
|
1337
1445
|
return parts.join(" \xB7 ");
|
|
1338
1446
|
}
|
|
1339
1447
|
function wide2(rows, layout, noun, o) {
|
|
@@ -1356,7 +1464,7 @@ function wide2(rows, layout, noun, o) {
|
|
|
1356
1464
|
marks += drawable(row.estimate) ? barMarks(row, x0, x1, y + 7, o) : text(x0, y + 18, estimateNote(row.estimate, row.attempts), { cls: "tgc-ink-muted" });
|
|
1357
1465
|
marks += text(knRight, y + 18, kn(row.solved, row.attempts), { cls: "tgc-ink tgc-num", anchor: "end" });
|
|
1358
1466
|
if (layout.cost) {
|
|
1359
|
-
const billed = row.cost
|
|
1467
|
+
const billed = perPass(row.cost) !== null && row.cost.basis === "receipts";
|
|
1360
1468
|
marks += text(costRight, y + 18, costText(row.cost, row.solved), {
|
|
1361
1469
|
cls: billed ? "tgc-ink tgc-num" : "tgc-ink-muted tgc-num",
|
|
1362
1470
|
anchor: "end"
|
|
@@ -1413,7 +1521,7 @@ function narrow(rows, layout, o) {
|
|
|
1413
1521
|
const facts = [
|
|
1414
1522
|
`${kn(row.solved, row.attempts)} passed`,
|
|
1415
1523
|
...drawable(row.estimate) ? [] : [estimateNote(row.estimate, row.attempts)],
|
|
1416
|
-
...layout.cost && !(row.solved === 0 && row.cost
|
|
1524
|
+
...layout.cost && !(row.solved === 0 && perPass(row.cost) === null) ? [perPass(row.cost) === null ? "cost unknown" : `${costText(row.cost, row.solved)} per pass`] : [],
|
|
1417
1525
|
...layout.median ? [row.medianWallMs === null ? "run time unknown" : `${duration(row.medianWallMs)} run`] : []
|
|
1418
1526
|
];
|
|
1419
1527
|
const factLines = fitLines(facts, NARROW, 12, "mono");
|
|
@@ -1503,7 +1611,7 @@ function describe(task, setup, cell, means) {
|
|
|
1503
1611
|
else parts.push(`${cell.attempts} ${plural(cell.attempts, "attempt")}`);
|
|
1504
1612
|
const tiers = tierSummary(cell.tiers, cell.unmeasured, cell.flagged);
|
|
1505
1613
|
if (tiers) parts.push(tiers);
|
|
1506
|
-
if (cell.costUsd !== null) parts.push(usd(cell.costUsd));
|
|
1614
|
+
if (cell.costUsd !== null) parts.push(cell.costUsd > 0 ? usd(cell.costUsd) : "cost unknown");
|
|
1507
1615
|
return parts.join(" \xB7 ").replace(`${setup.label} \xB7 `, `${setup.label}: `);
|
|
1508
1616
|
}
|
|
1509
1617
|
function signature(cell) {
|
|
@@ -1523,11 +1631,26 @@ function taskMatrix(tasks, setupsIn, cells, o = {}) {
|
|
|
1523
1631
|
return { id, refused: `A cell names ${cell.task} \xD7 ${cell.setup}, which is not in the ${noun.task} and ${noun.setup} lists.` };
|
|
1524
1632
|
}
|
|
1525
1633
|
if (byKey.has(key)) return { id, refused: `Two cells for ${cell.task} \xD7 ${cell.setup}.` };
|
|
1634
|
+
const counts = [cell.attempts, cell.solved, cell.unmeasured, cell.flagged];
|
|
1635
|
+
if (counts.some((n) => n !== null && n !== void 0 && (!Number.isInteger(n) || n < 0))) {
|
|
1636
|
+
return { id, refused: `${cell.task} \xD7 ${cell.setup}: counts must be nonnegative whole numbers.` };
|
|
1637
|
+
}
|
|
1638
|
+
if (cell.tiers && TIERS_BEST_FIRST.some((tier) => !Number.isInteger(cell.tiers[tier]) || cell.tiers[tier] < 0)) {
|
|
1639
|
+
return { id, refused: `${cell.task} \xD7 ${cell.setup}: tier counts must be nonnegative whole numbers.` };
|
|
1640
|
+
}
|
|
1641
|
+
if (cell.mean !== null && !Number.isFinite(cell.mean)) return { id, refused: `${cell.task} \xD7 ${cell.setup}: mean is not finite.` };
|
|
1642
|
+
if (cell.costUsd !== null && (!Number.isFinite(cell.costUsd) || cell.costUsd < 0)) {
|
|
1643
|
+
return { id, refused: `${cell.task} \xD7 ${cell.setup}: cost is invalid.` };
|
|
1644
|
+
}
|
|
1526
1645
|
if (!means && cell.attempts !== null) {
|
|
1527
1646
|
const graded = tierTotal(cell.tiers) + (cell.flagged ?? 0) + (cell.unmeasured ?? 0);
|
|
1528
1647
|
if (graded > cell.attempts || cell.solved !== null && cell.solved > cell.attempts) {
|
|
1529
1648
|
return { id, refused: `${cell.task} \xD7 ${cell.setup}: grades or passes exceed its ${cell.attempts} attempts.` };
|
|
1530
1649
|
}
|
|
1650
|
+
if (o.passTier && cell.tiers && cell.solved !== null) {
|
|
1651
|
+
const passing = TIERS_BEST_FIRST.filter((tier) => tierAtLeast(tier, o.passTier)).reduce((sum, tier) => sum + cell.tiers[tier], 0);
|
|
1652
|
+
if (passing !== cell.solved) return { id, refused: `${cell.task} \xD7 ${cell.setup}: ${cell.solved} passes disagree with ${passing} grades at ${o.passTier} or better.` };
|
|
1653
|
+
}
|
|
1531
1654
|
}
|
|
1532
1655
|
byKey.set(key, cell);
|
|
1533
1656
|
}
|
|
@@ -1599,7 +1722,7 @@ function taskMatrix(tasks, setupsIn, cells, o = {}) {
|
|
|
1599
1722
|
const cell = at(task, s);
|
|
1600
1723
|
const main = cellText(cell, means);
|
|
1601
1724
|
const extra = !means && cell && cell.solved !== null && cell.tiers ? ` \xB7 ${tierSummary(cell.tiers, cell.unmeasured, cell.flagged)}` : "";
|
|
1602
|
-
const cost = cell && cell.costUsd !== null ? ` \xB7 ${usd(cell.costUsd)}` : "";
|
|
1725
|
+
const cost = cell && cell.costUsd !== null ? ` \xB7 ${cell.costUsd > 0 ? usd(cell.costUsd) : "cost unknown"}` : "";
|
|
1603
1726
|
return main === "\xB7" ? "not run" : `${main}${extra}${cost}`;
|
|
1604
1727
|
})
|
|
1605
1728
|
])
|
|
@@ -1764,6 +1887,7 @@ export {
|
|
|
1764
1887
|
figureCss,
|
|
1765
1888
|
isRefusal,
|
|
1766
1889
|
rankedRates,
|
|
1890
|
+
rateOrder,
|
|
1767
1891
|
renderFigure,
|
|
1768
1892
|
renderNotes,
|
|
1769
1893
|
renderRefusals,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/charts",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Benchmark figures as static SVG and HTML tables: ranked rates, cost frontier, comparison intervals, task matrix and breakdown. Draws from producer data and refuses what it cannot draw honestly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/tangle-network/brand",
|
|
9
|
+
"url": "git+https://github.com/tangle-network/brand.git",
|
|
10
10
|
"directory": "packages/charts"
|
|
11
11
|
},
|
|
12
12
|
"bugs": {
|
|
@@ -28,10 +28,6 @@
|
|
|
28
28
|
},
|
|
29
29
|
"./figure.css": "./dist/figure.css"
|
|
30
30
|
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"gen:css": "node scripts/gen-css.mjs",
|
|
33
|
-
"build": "node scripts/gen-css.mjs --check && tsup src/index.ts --format esm --dts --clean && cp src/figure.css dist/figure.css"
|
|
34
|
-
},
|
|
35
31
|
"devDependencies": {
|
|
36
32
|
"tsup": "^8.3.5",
|
|
37
33
|
"typescript": "^5.6.0"
|
|
@@ -41,5 +37,9 @@
|
|
|
41
37
|
"charts",
|
|
42
38
|
"benchmarks",
|
|
43
39
|
"svg"
|
|
44
|
-
]
|
|
45
|
-
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"gen:css": "node scripts/gen-css.mjs",
|
|
43
|
+
"build": "node scripts/gen-css.mjs --check && tsup src/index.ts --format esm --dts --clean && cp src/figure.css dist/figure.css"
|
|
44
|
+
}
|
|
45
|
+
}
|