Spaces:
Running
feat: refine leaderboard visualizations and CSV exports
Browse filesPolish the Ranking, Details, and Visualizations experiences with clearer
sorting states, reusable cursor tooltips, better chart labels, and working
CSV exports.
- add reusable cursor-based tooltip components for charts and overflowing text
- refactor OverflowTooltip to use the new cursor tooltip behavior
- add RadarLabelsOverlay for radar chart labels that can truncate and reveal
full names on hover
- add global thin scrollbar styling for light and dark themes
- allow table column header/cell classes to be computed reactively so active
sort columns can be highlighted
Ranking:
- show active sort direction with up/down icons instead of a static sort icon
- highlight the active sorted column in headers and cells
- add overflow tooltip support for long model names
- display model params consistently in filter rows and table summaries
Details:
- add CSV serialization for benchmark matrix rows and filtered model rows
- replace inactive matrix export buttons with a single working CSV export
- wire the models tab CSV button to export the current filtered model list
- improve benchmark matrix sorting indicators and active-column highlighting
- add overflow tooltips for long matrix dataset and model group labels
- render model radar labels through RadarLabelsOverlay for cleaner clipping
Visualizations:
- start with the first five models selected by default for a cleaner initial view
- move chart tooltip state into the shared ChartCursorTooltip component
- add pinned-model support across comparison charts and Pareto labels
- improve radar chart labels and hover details
- show an empty heatmap state when no models or groups are selected
- change grouped bars to compare models by averaged selected-group metrics
- fix scatter axes to a stable 0..1 scale
- simplify Pareto copy and axis labels, and keep pinned labels visible
- highlight active latency table sort columns
- include FPR real/robust/delta values in robustness rows alongside FNR
Data and types:
- parse optional robustness FPR fields from bucket visualization payloads
- propagate FPR robustness fields through bucket adapters and frontend types
- remove obsolete chart tooltip types from tools types
Localization:
- add heatmap empty-state text
- add grouped average summary text
- update grouped bars and Pareto descriptions in English and Russian
- messages/en.json +7 -6
- messages/ru.json +7 -6
- src/app.css +25 -0
- src/lib/components/OverflowTooltip.svelte +8 -45
- src/lib/components/Table.svelte +8 -4
- src/lib/components/charts/RadarLabelsOverlay.svelte +60 -0
- src/lib/components/tooltips/ChartCursorTooltip.svelte +96 -0
- src/lib/components/tooltips/CursorTooltip.svelte +81 -0
- src/lib/components/tooltips/OverflowCursorTooltip.svelte +159 -0
- src/lib/data/details.ts +88 -0
- src/lib/server/hf-bucket/adapt-tools.ts +3 -0
- src/lib/server/hf-bucket/types.ts +3 -0
- src/lib/server/hf-bucket/visualizations-payload.ts +3 -0
- src/lib/types/tools-data.ts +3 -0
- src/lib/types/tools.ts +0 -12
- src/routes/+page.svelte +39 -8
- src/routes/details/+page.svelte +158 -66
- src/routes/tools/+page.svelte +1047 -992
|
@@ -253,6 +253,7 @@
|
|
| 253 |
"tools_radar_scale": "Radar scale",
|
| 254 |
"tools_radar_group_score": "group score",
|
| 255 |
"tools_mean": "mean",
|
|
|
|
| 256 |
"tools_real_score": "score real",
|
| 257 |
"tools_robust_score": "score robust",
|
| 258 |
"tools_selected_summary": "{models} models · {groups} groups",
|
|
@@ -269,17 +270,17 @@
|
|
| 269 |
"tools_heatmap_scale_min": "min {value}",
|
| 270 |
"tools_heatmap_scale_max": "max {value}",
|
| 271 |
"tools_heatmap_header": "{metric} · group / model",
|
|
|
|
| 272 |
"tools_grouped_bars_title": "Grouped bars",
|
| 273 |
-
"tools_grouped_bars_hint": "FNR / FPR / F1 by
|
| 274 |
-
"tools_grouped_bars_aria": "Grouped FNR FPR F1 bars by
|
| 275 |
"tools_lower_better": "lower = better",
|
| 276 |
"tools_higher_better": "higher = better",
|
| 277 |
"tools_pareto_title": "Pareto: quality × latency",
|
| 278 |
-
"tools_pareto_hint": "x = p95 latency (ms, lower = better), y = integral (higher = better)
|
| 279 |
"tools_pareto_aria": "Pareto quality versus p95 latency",
|
| 280 |
-
"tools_pareto_x_axis": "p95
|
| 281 |
-
"tools_pareto_y_axis": "
|
| 282 |
-
"tools_pareto_fpr_legend": "teal (few false alarms) → red (many)",
|
| 283 |
"tools_pareto_click_pin_hint": "hover for details · dashed line = Pareto frontier",
|
| 284 |
"tools_pareto_pin_model": "Pin {model} label",
|
| 285 |
"tools_pareto_unpin_model": "Unpin {model} label",
|
|
|
|
| 253 |
"tools_radar_scale": "Radar scale",
|
| 254 |
"tools_radar_group_score": "group score",
|
| 255 |
"tools_mean": "mean",
|
| 256 |
+
"tools_group_mean_summary": "average across {groups} groups",
|
| 257 |
"tools_real_score": "score real",
|
| 258 |
"tools_robust_score": "score robust",
|
| 259 |
"tools_selected_summary": "{models} models · {groups} groups",
|
|
|
|
| 270 |
"tools_heatmap_scale_min": "min {value}",
|
| 271 |
"tools_heatmap_scale_max": "max {value}",
|
| 272 |
"tools_heatmap_header": "{metric} · group / model",
|
| 273 |
+
"tools_heatmap_empty": "Select at least one model and one group to show the heatmap.",
|
| 274 |
"tools_grouped_bars_title": "Grouped bars",
|
| 275 |
+
"tools_grouped_bars_hint": "FNR / FPR / F1 by model · averages across selected groups",
|
| 276 |
+
"tools_grouped_bars_aria": "Grouped FNR FPR F1 bars by model",
|
| 277 |
"tools_lower_better": "lower = better",
|
| 278 |
"tools_higher_better": "higher = better",
|
| 279 |
"tools_pareto_title": "Pareto: quality × latency",
|
| 280 |
+
"tools_pareto_hint": "x = p95 latency (ms, lower = better), y = integral (higher = better)",
|
| 281 |
"tools_pareto_aria": "Pareto quality versus p95 latency",
|
| 282 |
+
"tools_pareto_x_axis": "p95 ms ↓",
|
| 283 |
+
"tools_pareto_y_axis": "Integral ↑",
|
|
|
|
| 284 |
"tools_pareto_click_pin_hint": "hover for details · dashed line = Pareto frontier",
|
| 285 |
"tools_pareto_pin_model": "Pin {model} label",
|
| 286 |
"tools_pareto_unpin_model": "Unpin {model} label",
|
|
@@ -253,6 +253,7 @@
|
|
| 253 |
"tools_radar_scale": "Шкала радара",
|
| 254 |
"tools_radar_group_score": "group score",
|
| 255 |
"tools_mean": "среднее",
|
|
|
|
| 256 |
"tools_real_score": "score real",
|
| 257 |
"tools_robust_score": "score robust",
|
| 258 |
"tools_selected_summary": "{models} моделей · {groups} групп",
|
|
@@ -269,17 +270,17 @@
|
|
| 269 |
"tools_heatmap_scale_min": "min {value}",
|
| 270 |
"tools_heatmap_scale_max": "max {value}",
|
| 271 |
"tools_heatmap_header": "{metric} · группа / модель",
|
|
|
|
| 272 |
"tools_grouped_bars_title": "Сгруппированные бары",
|
| 273 |
-
"tools_grouped_bars_hint": "FNR / FPR / F1 по
|
| 274 |
-
"tools_grouped_bars_aria": "Сгруппированные бары FNR FPR F1 по
|
| 275 |
"tools_lower_better": "ниже = лучше",
|
| 276 |
"tools_higher_better": "выше = лучше",
|
| 277 |
"tools_pareto_title": "Парето: качество × задержка",
|
| 278 |
-
"tools_pareto_hint": "x = p95 latency (ms, ниже = лучше), y = integral (выше = лучше)
|
| 279 |
"tools_pareto_aria": "Парето качество против задержки p95",
|
| 280 |
-
"tools_pareto_x_axis": "p95
|
| 281 |
-
"tools_pareto_y_axis": "
|
| 282 |
-
"tools_pareto_fpr_legend": "teal (мало ложных) → red (много)",
|
| 283 |
"tools_pareto_click_pin_hint": "hover — детали · пунктир = Pareto frontier",
|
| 284 |
"tools_pareto_pin_model": "Закрепить подпись {model}",
|
| 285 |
"tools_pareto_unpin_model": "Снять подпись {model}",
|
|
|
|
| 253 |
"tools_radar_scale": "Шкала радара",
|
| 254 |
"tools_radar_group_score": "group score",
|
| 255 |
"tools_mean": "среднее",
|
| 256 |
+
"tools_group_mean_summary": "среднее по {groups} группам",
|
| 257 |
"tools_real_score": "score real",
|
| 258 |
"tools_robust_score": "score robust",
|
| 259 |
"tools_selected_summary": "{models} моделей · {groups} групп",
|
|
|
|
| 270 |
"tools_heatmap_scale_min": "min {value}",
|
| 271 |
"tools_heatmap_scale_max": "max {value}",
|
| 272 |
"tools_heatmap_header": "{metric} · группа / модель",
|
| 273 |
+
"tools_heatmap_empty": "Выберите хотя бы одну модель и одну группу, чтобы показать heatmap.",
|
| 274 |
"tools_grouped_bars_title": "Сгруппированные бары",
|
| 275 |
+
"tools_grouped_bars_hint": "FNR / FPR / F1 по моделям · средние по выбранным группам",
|
| 276 |
+
"tools_grouped_bars_aria": "Сгруппированные бары FNR FPR F1 по моделям",
|
| 277 |
"tools_lower_better": "ниже = лучше",
|
| 278 |
"tools_higher_better": "выше = лучше",
|
| 279 |
"tools_pareto_title": "Парето: качество × задержка",
|
| 280 |
+
"tools_pareto_hint": "x = p95 latency (ms, ниже = лучше), y = integral (выше = лучше)",
|
| 281 |
"tools_pareto_aria": "Парето качество против задержки p95",
|
| 282 |
+
"tools_pareto_x_axis": "p95 ms ↓",
|
| 283 |
+
"tools_pareto_y_axis": "Integral ↑",
|
|
|
|
| 284 |
"tools_pareto_click_pin_hint": "hover — детали · пунктир = Pareto frontier",
|
| 285 |
"tools_pareto_pin_model": "Закрепить подпись {model}",
|
| 286 |
"tools_pareto_unpin_model": "Снять подпись {model}",
|
|
@@ -39,6 +39,8 @@
|
|
| 39 |
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
| 40 |
--sidebar-border: oklch(0.92 0.004 286.32);
|
| 41 |
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
|
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
.dark {
|
|
@@ -73,6 +75,8 @@
|
|
| 73 |
--sidebar-accent-foreground: oklch(0.985 0 0);
|
| 74 |
--sidebar-border: oklch(1 0 0 / 10%);
|
| 75 |
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
@theme inline {
|
|
@@ -120,6 +124,27 @@
|
|
| 120 |
@layer base {
|
| 121 |
* {
|
| 122 |
@apply border-border outline-ring/50;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
body {
|
| 125 |
@apply bg-background text-foreground;
|
|
|
|
| 39 |
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
| 40 |
--sidebar-border: oklch(0.92 0.004 286.32);
|
| 41 |
--sidebar-ring: oklch(0.705 0.015 286.067);
|
| 42 |
+
--scrollbar-thumb: oklch(0.705 0.015 286.067 / 65%);
|
| 43 |
+
--scrollbar-thumb-hover: oklch(0.552 0.016 285.938 / 85%);
|
| 44 |
}
|
| 45 |
|
| 46 |
.dark {
|
|
|
|
| 75 |
--sidebar-accent-foreground: oklch(0.985 0 0);
|
| 76 |
--sidebar-border: oklch(1 0 0 / 10%);
|
| 77 |
--sidebar-ring: oklch(0.552 0.016 285.938);
|
| 78 |
+
--scrollbar-thumb: oklch(0.552 0.016 285.938 / 70%);
|
| 79 |
+
--scrollbar-thumb-hover: oklch(0.705 0.015 286.067 / 90%);
|
| 80 |
}
|
| 81 |
|
| 82 |
@theme inline {
|
|
|
|
| 124 |
@layer base {
|
| 125 |
* {
|
| 126 |
@apply border-border outline-ring/50;
|
| 127 |
+
scrollbar-color: var(--scrollbar-thumb) transparent;
|
| 128 |
+
scrollbar-width: thin;
|
| 129 |
+
}
|
| 130 |
+
*::-webkit-scrollbar {
|
| 131 |
+
width: 10px;
|
| 132 |
+
height: 10px;
|
| 133 |
+
}
|
| 134 |
+
*::-webkit-scrollbar-track {
|
| 135 |
+
background: transparent;
|
| 136 |
+
}
|
| 137 |
+
*::-webkit-scrollbar-thumb {
|
| 138 |
+
border: 3px solid transparent;
|
| 139 |
+
border-radius: 999px;
|
| 140 |
+
background-color: var(--scrollbar-thumb);
|
| 141 |
+
background-clip: content-box;
|
| 142 |
+
}
|
| 143 |
+
*::-webkit-scrollbar-thumb:hover {
|
| 144 |
+
background-color: var(--scrollbar-thumb-hover);
|
| 145 |
+
}
|
| 146 |
+
*::-webkit-scrollbar-corner {
|
| 147 |
+
background: transparent;
|
| 148 |
}
|
| 149 |
body {
|
| 150 |
@apply bg-background text-foreground;
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import
|
| 3 |
-
import
|
| 4 |
|
| 5 |
type Props = {
|
| 6 |
text: string;
|
|
@@ -15,48 +15,11 @@
|
|
| 15 |
triggerClass = 'block min-w-0',
|
| 16 |
contentClass = 'max-w-sm break-all'
|
| 17 |
}: Props = $props();
|
| 18 |
-
let element: HTMLElement | null = $state(null);
|
| 19 |
-
let isOverflowing = $state(false);
|
| 20 |
-
|
| 21 |
-
function updateOverflow() {
|
| 22 |
-
if (!element) {
|
| 23 |
-
isOverflowing = false;
|
| 24 |
-
return;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
isOverflowing =
|
| 28 |
-
element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
$effect(() => {
|
| 32 |
-
const currentText = text;
|
| 33 |
-
tick().then(() => {
|
| 34 |
-
if (currentText === text) updateOverflow();
|
| 35 |
-
});
|
| 36 |
-
});
|
| 37 |
-
|
| 38 |
-
$effect(() => {
|
| 39 |
-
if (!element || typeof ResizeObserver === 'undefined') return;
|
| 40 |
-
|
| 41 |
-
const observer = new ResizeObserver(updateOverflow);
|
| 42 |
-
observer.observe(element);
|
| 43 |
-
updateOverflow();
|
| 44 |
-
|
| 45 |
-
return () => observer.disconnect();
|
| 46 |
-
});
|
| 47 |
</script>
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
{
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
{@render label()}
|
| 56 |
-
{#snippet content()}
|
| 57 |
-
{text}
|
| 58 |
-
{/snippet}
|
| 59 |
-
</Tooltip>
|
| 60 |
-
{:else}
|
| 61 |
-
{@render label()}
|
| 62 |
-
{/if}
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import OverflowCursorTooltip from '$lib/components/tooltips/OverflowCursorTooltip.svelte';
|
| 3 |
+
import { cn } from '$lib/utils';
|
| 4 |
|
| 5 |
type Props = {
|
| 6 |
text: string;
|
|
|
|
| 15 |
triggerClass = 'block min-w-0',
|
| 16 |
contentClass = 'max-w-sm break-all'
|
| 17 |
}: Props = $props();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
</script>
|
| 19 |
|
| 20 |
+
<OverflowCursorTooltip
|
| 21 |
+
{text}
|
| 22 |
+
labelClass={cn(triggerClass, className)}
|
| 23 |
+
tooltipClass={contentClass}
|
| 24 |
+
position="label"
|
| 25 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -4,8 +4,8 @@
|
|
| 4 |
export type TableColumn<Key extends string = string> = {
|
| 5 |
key: Key;
|
| 6 |
label: string | (() => string);
|
| 7 |
-
headClass?: string;
|
| 8 |
-
cellClass?: string;
|
| 9 |
ariaSort?: () => 'ascending' | 'descending' | 'none';
|
| 10 |
};
|
| 11 |
</script>
|
|
@@ -52,6 +52,10 @@
|
|
| 52 |
return typeof rowClass === 'function' ? rowClass(row, index) : rowClass;
|
| 53 |
}
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
function handleRowKeydown(event: KeyboardEvent, row: Row, index: number) {
|
| 56 |
if (!onrowclick || (event.key !== 'Enter' && event.key !== ' ')) return;
|
| 57 |
|
|
@@ -70,7 +74,7 @@
|
|
| 70 |
<UITable.Row class={headerClass}>
|
| 71 |
{#each columns as column (column.key)}
|
| 72 |
<UITable.Head
|
| 73 |
-
class={cn('text-muted-foreground', column.headClass)}
|
| 74 |
aria-sort={column.ariaSort?.() ?? 'none'}
|
| 75 |
>
|
| 76 |
{#if header}
|
|
@@ -93,7 +97,7 @@
|
|
| 93 |
onkeydown={(event) => handleRowKeydown(event, row, index)}
|
| 94 |
>
|
| 95 |
{#each columns as column (column.key)}
|
| 96 |
-
<UITable.Cell class={column.cellClass}>
|
| 97 |
{@render cell(row, column, index)}
|
| 98 |
</UITable.Cell>
|
| 99 |
{/each}
|
|
|
|
| 4 |
export type TableColumn<Key extends string = string> = {
|
| 5 |
key: Key;
|
| 6 |
label: string | (() => string);
|
| 7 |
+
headClass?: string | (() => string | undefined);
|
| 8 |
+
cellClass?: string | (() => string | undefined);
|
| 9 |
ariaSort?: () => 'ascending' | 'descending' | 'none';
|
| 10 |
};
|
| 11 |
</script>
|
|
|
|
| 52 |
return typeof rowClass === 'function' ? rowClass(row, index) : rowClass;
|
| 53 |
}
|
| 54 |
|
| 55 |
+
function resolvedColumnClass(className: string | (() => string | undefined) | undefined) {
|
| 56 |
+
return typeof className === 'function' ? className() : className;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
function handleRowKeydown(event: KeyboardEvent, row: Row, index: number) {
|
| 60 |
if (!onrowclick || (event.key !== 'Enter' && event.key !== ' ')) return;
|
| 61 |
|
|
|
|
| 74 |
<UITable.Row class={headerClass}>
|
| 75 |
{#each columns as column (column.key)}
|
| 76 |
<UITable.Head
|
| 77 |
+
class={cn('text-muted-foreground', resolvedColumnClass(column.headClass))}
|
| 78 |
aria-sort={column.ariaSort?.() ?? 'none'}
|
| 79 |
>
|
| 80 |
{#if header}
|
|
|
|
| 97 |
onkeydown={(event) => handleRowKeydown(event, row, index)}
|
| 98 |
>
|
| 99 |
{#each columns as column (column.key)}
|
| 100 |
+
<UITable.Cell class={resolvedColumnClass(column.cellClass)}>
|
| 101 |
{@render cell(row, column, index)}
|
| 102 |
</UITable.Cell>
|
| 103 |
{/each}
|
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script module lang="ts">
|
| 2 |
+
export type RadarLabelAnchor = 'start' | 'middle' | 'end';
|
| 3 |
+
|
| 4 |
+
export type RadarOverlayLabel = {
|
| 5 |
+
key: string;
|
| 6 |
+
text: string;
|
| 7 |
+
x: number;
|
| 8 |
+
y: number;
|
| 9 |
+
anchor: RadarLabelAnchor;
|
| 10 |
+
};
|
| 11 |
+
</script>
|
| 12 |
+
|
| 13 |
+
<script lang="ts">
|
| 14 |
+
import OverflowCursorTooltip from '$lib/components/tooltips/OverflowCursorTooltip.svelte';
|
| 15 |
+
import { cn } from '$lib/utils';
|
| 16 |
+
|
| 17 |
+
type Props = {
|
| 18 |
+
labels: readonly RadarOverlayLabel[];
|
| 19 |
+
viewBoxWidth: number;
|
| 20 |
+
viewBoxHeight: number;
|
| 21 |
+
labelClass?: string;
|
| 22 |
+
tooltipClass?: string;
|
| 23 |
+
position?: 'cursor' | 'label';
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
let {
|
| 27 |
+
labels,
|
| 28 |
+
viewBoxWidth,
|
| 29 |
+
viewBoxHeight,
|
| 30 |
+
labelClass,
|
| 31 |
+
tooltipClass,
|
| 32 |
+
position = 'cursor'
|
| 33 |
+
}: Props = $props();
|
| 34 |
+
|
| 35 |
+
function labelStyle(label: RadarOverlayLabel) {
|
| 36 |
+
return `left:${(label.x / viewBoxWidth) * 100}%; top:${(label.y / viewBoxHeight) * 100}%;`;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
function labelAlignmentClass(anchor: RadarLabelAnchor) {
|
| 40 |
+
if (anchor === 'middle') return '-translate-x-1/2 text-center';
|
| 41 |
+
if (anchor === 'start') return 'text-left';
|
| 42 |
+
return '-translate-x-full text-right';
|
| 43 |
+
}
|
| 44 |
+
</script>
|
| 45 |
+
|
| 46 |
+
<div class="pointer-events-none absolute inset-0">
|
| 47 |
+
{#each labels as label (label.key)}
|
| 48 |
+
<OverflowCursorTooltip
|
| 49 |
+
text={label.text}
|
| 50 |
+
labelClass={cn(
|
| 51 |
+
'pointer-events-auto absolute block w-28 -translate-y-1/2 truncate font-mono text-[10px] leading-none text-muted-foreground',
|
| 52 |
+
labelAlignmentClass(label.anchor),
|
| 53 |
+
labelClass
|
| 54 |
+
)}
|
| 55 |
+
labelStyle={labelStyle(label)}
|
| 56 |
+
{tooltipClass}
|
| 57 |
+
{position}
|
| 58 |
+
/>
|
| 59 |
+
{/each}
|
| 60 |
+
</div>
|
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script module lang="ts">
|
| 2 |
+
import type { CursorTooltipRow } from './CursorTooltip.svelte';
|
| 3 |
+
|
| 4 |
+
export type ChartCursorTooltipApi = {
|
| 5 |
+
show: (event: PointerEvent, title: string, rows: readonly CursorTooltipRow[]) => void;
|
| 6 |
+
move: (event: PointerEvent) => void;
|
| 7 |
+
hide: () => void;
|
| 8 |
+
};
|
| 9 |
+
</script>
|
| 10 |
+
|
| 11 |
+
<script lang="ts">
|
| 12 |
+
import { tick } from 'svelte';
|
| 13 |
+
import type { Snippet } from 'svelte';
|
| 14 |
+
import CursorTooltip from './CursorTooltip.svelte';
|
| 15 |
+
|
| 16 |
+
type Props = {
|
| 17 |
+
children: Snippet<[ChartCursorTooltipApi]>;
|
| 18 |
+
class?: string;
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
let { children, class: className }: Props = $props();
|
| 22 |
+
|
| 23 |
+
let tooltipElement: HTMLDivElement | null = $state(null);
|
| 24 |
+
let tooltip = $state<{
|
| 25 |
+
x: number;
|
| 26 |
+
y: number;
|
| 27 |
+
title: string;
|
| 28 |
+
rows: readonly CursorTooltipRow[];
|
| 29 |
+
} | null>(null);
|
| 30 |
+
|
| 31 |
+
function tooltipPosition(event: Pick<PointerEvent, 'clientX' | 'clientY'>) {
|
| 32 |
+
const margin = 12;
|
| 33 |
+
const offset = 16;
|
| 34 |
+
const tooltipRect = tooltipElement?.getBoundingClientRect();
|
| 35 |
+
const tooltipWidth = tooltipRect?.width ?? 384;
|
| 36 |
+
const tooltipHeight = tooltipRect?.height ?? 180;
|
| 37 |
+
let x = event.clientX + offset;
|
| 38 |
+
let y = event.clientY + offset;
|
| 39 |
+
|
| 40 |
+
if (typeof window === 'undefined') return { x, y };
|
| 41 |
+
|
| 42 |
+
if (x + tooltipWidth + margin > window.innerWidth) {
|
| 43 |
+
x = event.clientX - tooltipWidth - offset;
|
| 44 |
+
}
|
| 45 |
+
if (y + tooltipHeight + margin > window.innerHeight) {
|
| 46 |
+
y = event.clientY - tooltipHeight - offset;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
return {
|
| 50 |
+
x: Math.min(Math.max(margin, x), Math.max(margin, window.innerWidth - tooltipWidth - margin)),
|
| 51 |
+
y: Math.min(
|
| 52 |
+
Math.max(margin, y),
|
| 53 |
+
Math.max(margin, window.innerHeight - tooltipHeight - margin)
|
| 54 |
+
)
|
| 55 |
+
};
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function show(event: PointerEvent, title: string, rows: readonly CursorTooltipRow[]) {
|
| 59 |
+
const position = tooltipPosition(event);
|
| 60 |
+
tooltip = {
|
| 61 |
+
x: position.x,
|
| 62 |
+
y: position.y,
|
| 63 |
+
title,
|
| 64 |
+
rows
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
tick().then(() => {
|
| 68 |
+
if (!tooltip) return;
|
| 69 |
+
tooltip = { ...tooltip, ...tooltipPosition(event) };
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function move(event: PointerEvent) {
|
| 74 |
+
if (!tooltip) return;
|
| 75 |
+
tooltip = { ...tooltip, ...tooltipPosition(event) };
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function hide() {
|
| 79 |
+
tooltip = null;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
const api: ChartCursorTooltipApi = { show, move, hide };
|
| 83 |
+
</script>
|
| 84 |
+
|
| 85 |
+
{@render children(api)}
|
| 86 |
+
|
| 87 |
+
{#if tooltip}
|
| 88 |
+
<CursorTooltip
|
| 89 |
+
bind:ref={tooltipElement}
|
| 90 |
+
x={tooltip.x}
|
| 91 |
+
y={tooltip.y}
|
| 92 |
+
title={tooltip.title}
|
| 93 |
+
rows={tooltip.rows}
|
| 94 |
+
class={className}
|
| 95 |
+
/>
|
| 96 |
+
{/if}
|
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script module lang="ts">
|
| 2 |
+
export type CursorTooltipRow = {
|
| 3 |
+
label: string;
|
| 4 |
+
value: string;
|
| 5 |
+
};
|
| 6 |
+
</script>
|
| 7 |
+
|
| 8 |
+
<script lang="ts">
|
| 9 |
+
import type { Snippet } from 'svelte';
|
| 10 |
+
import { cn } from '$lib/utils';
|
| 11 |
+
|
| 12 |
+
type Props = {
|
| 13 |
+
x: number;
|
| 14 |
+
y: number;
|
| 15 |
+
title?: string;
|
| 16 |
+
rows?: readonly CursorTooltipRow[];
|
| 17 |
+
children?: Snippet;
|
| 18 |
+
class?: string;
|
| 19 |
+
titleClass?: string;
|
| 20 |
+
rowsClass?: string;
|
| 21 |
+
rowClass?: string;
|
| 22 |
+
labelClass?: string;
|
| 23 |
+
valueClass?: string;
|
| 24 |
+
ref?: HTMLDivElement | null;
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
let {
|
| 28 |
+
x,
|
| 29 |
+
y,
|
| 30 |
+
title,
|
| 31 |
+
rows = [],
|
| 32 |
+
children,
|
| 33 |
+
class: className,
|
| 34 |
+
titleClass,
|
| 35 |
+
rowsClass,
|
| 36 |
+
rowClass,
|
| 37 |
+
labelClass,
|
| 38 |
+
valueClass,
|
| 39 |
+
ref = $bindable(null)
|
| 40 |
+
}: Props = $props();
|
| 41 |
+
|
| 42 |
+
function portalTooltip(node: HTMLDivElement) {
|
| 43 |
+
if (typeof document === 'undefined') return;
|
| 44 |
+
|
| 45 |
+
document.body.appendChild(node);
|
| 46 |
+
|
| 47 |
+
return {
|
| 48 |
+
destroy() {
|
| 49 |
+
node.remove();
|
| 50 |
+
}
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
</script>
|
| 54 |
+
|
| 55 |
+
<div
|
| 56 |
+
use:portalTooltip
|
| 57 |
+
bind:this={ref}
|
| 58 |
+
class={cn(
|
| 59 |
+
'pointer-events-none fixed z-[100] max-w-[min(24rem,calc(100vw-1.5rem))] rounded-lg border bg-popover px-4 py-3 text-sm text-popover-foreground shadow-xl',
|
| 60 |
+
className
|
| 61 |
+
)}
|
| 62 |
+
style={`left:${x}px; top:${y}px`}
|
| 63 |
+
>
|
| 64 |
+
{#if children}
|
| 65 |
+
{@render children()}
|
| 66 |
+
{:else}
|
| 67 |
+
{#if title}
|
| 68 |
+
<div class={cn('break-words font-semibold', titleClass)}>{title}</div>
|
| 69 |
+
{/if}
|
| 70 |
+
{#if rows.length}
|
| 71 |
+
<div class={cn('mt-2 space-y-1 font-mono', rowsClass)}>
|
| 72 |
+
{#each rows as row (`${row.label}-${row.value}`)}
|
| 73 |
+
<div class={cn('grid grid-cols-[auto_1fr] gap-5', rowClass)}>
|
| 74 |
+
<span class={cn('break-words text-muted-foreground', labelClass)}>{row.label}</span>
|
| 75 |
+
<span class={cn('text-right', valueClass)}>{row.value}</span>
|
| 76 |
+
</div>
|
| 77 |
+
{/each}
|
| 78 |
+
</div>
|
| 79 |
+
{/if}
|
| 80 |
+
{/if}
|
| 81 |
+
</div>
|
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { tick } from 'svelte';
|
| 3 |
+
import type { Snippet } from 'svelte';
|
| 4 |
+
import CursorTooltip from './CursorTooltip.svelte';
|
| 5 |
+
|
| 6 |
+
type Props = {
|
| 7 |
+
text: string;
|
| 8 |
+
children?: Snippet;
|
| 9 |
+
labelClass?: string;
|
| 10 |
+
labelStyle?: string;
|
| 11 |
+
tooltipClass?: string;
|
| 12 |
+
role?: string;
|
| 13 |
+
ariaLabel?: string;
|
| 14 |
+
overflowPadding?: number;
|
| 15 |
+
position?: 'cursor' | 'label';
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
let {
|
| 19 |
+
text,
|
| 20 |
+
children,
|
| 21 |
+
labelClass = 'block min-w-0 truncate',
|
| 22 |
+
labelStyle,
|
| 23 |
+
tooltipClass,
|
| 24 |
+
role = 'note',
|
| 25 |
+
ariaLabel,
|
| 26 |
+
overflowPadding = 2,
|
| 27 |
+
position = 'cursor'
|
| 28 |
+
}: Props = $props();
|
| 29 |
+
|
| 30 |
+
let labelElement: HTMLSpanElement | null = $state(null);
|
| 31 |
+
let tooltipElement: HTMLDivElement | null = $state(null);
|
| 32 |
+
let tooltip = $state<{ x: number; y: number } | null>(null);
|
| 33 |
+
let measureCanvas: HTMLCanvasElement | undefined;
|
| 34 |
+
|
| 35 |
+
function textOverflows() {
|
| 36 |
+
if (!labelElement) return false;
|
| 37 |
+
|
| 38 |
+
const availableWidth = labelElement.clientWidth;
|
| 39 |
+
const label = text.trim();
|
| 40 |
+
if (availableWidth <= 0 || label === '') return false;
|
| 41 |
+
|
| 42 |
+
if (typeof document === 'undefined') {
|
| 43 |
+
return labelElement.scrollWidth > availableWidth + overflowPadding;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
measureCanvas ??= document.createElement('canvas');
|
| 47 |
+
const context = measureCanvas.getContext('2d');
|
| 48 |
+
if (!context) return labelElement.scrollWidth > availableWidth + overflowPadding;
|
| 49 |
+
|
| 50 |
+
context.font = getComputedStyle(labelElement).font;
|
| 51 |
+
return context.measureText(label).width > availableWidth + overflowPadding;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function clampTooltipPosition(x: number, y: number, tooltipWidth: number, tooltipHeight: number) {
|
| 55 |
+
const margin = 12;
|
| 56 |
+
if (typeof window === 'undefined') return { x, y };
|
| 57 |
+
|
| 58 |
+
if (x + tooltipWidth + margin > window.innerWidth) {
|
| 59 |
+
x = window.innerWidth - tooltipWidth - margin;
|
| 60 |
+
}
|
| 61 |
+
if (y + tooltipHeight + margin > window.innerHeight) {
|
| 62 |
+
y = window.innerHeight - tooltipHeight - margin;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
return {
|
| 66 |
+
x: Math.min(Math.max(margin, x), Math.max(margin, window.innerWidth - tooltipWidth - margin)),
|
| 67 |
+
y: Math.min(
|
| 68 |
+
Math.max(margin, y),
|
| 69 |
+
Math.max(margin, window.innerHeight - tooltipHeight - margin)
|
| 70 |
+
)
|
| 71 |
+
};
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function cursorTooltipPosition(event: Pick<PointerEvent, 'clientX' | 'clientY'>) {
|
| 75 |
+
const offset = 16;
|
| 76 |
+
const tooltipRect = tooltipElement?.getBoundingClientRect();
|
| 77 |
+
const tooltipWidth = tooltipRect?.width ?? 320;
|
| 78 |
+
const tooltipHeight = tooltipRect?.height ?? 64;
|
| 79 |
+
let x = event.clientX + offset;
|
| 80 |
+
let y = event.clientY + offset;
|
| 81 |
+
|
| 82 |
+
if (typeof window !== 'undefined') {
|
| 83 |
+
if (x + tooltipWidth + 12 > window.innerWidth) {
|
| 84 |
+
x = event.clientX - tooltipWidth - offset;
|
| 85 |
+
}
|
| 86 |
+
if (y + tooltipHeight + 12 > window.innerHeight) {
|
| 87 |
+
y = event.clientY - tooltipHeight - offset;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
return clampTooltipPosition(x, y, tooltipWidth, tooltipHeight);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
function labelTooltipPosition() {
|
| 95 |
+
const labelRect = labelElement?.getBoundingClientRect();
|
| 96 |
+
const tooltipRect = tooltipElement?.getBoundingClientRect();
|
| 97 |
+
const tooltipWidth = tooltipRect?.width ?? 320;
|
| 98 |
+
const tooltipHeight = tooltipRect?.height ?? 64;
|
| 99 |
+
const offset = 12;
|
| 100 |
+
if (!labelRect) return { x: 12, y: 12 };
|
| 101 |
+
|
| 102 |
+
const x = labelRect.left + labelRect.width / 2 - tooltipWidth / 2;
|
| 103 |
+
const y =
|
| 104 |
+
labelRect.top >= tooltipHeight + offset + 12
|
| 105 |
+
? labelRect.top - tooltipHeight - offset
|
| 106 |
+
: labelRect.bottom + offset;
|
| 107 |
+
|
| 108 |
+
return clampTooltipPosition(x, y, tooltipWidth, tooltipHeight);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function tooltipPosition(event: PointerEvent) {
|
| 112 |
+
return position === 'label' ? labelTooltipPosition() : cursorTooltipPosition(event);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
function updateTooltip(event: PointerEvent) {
|
| 116 |
+
if (!textOverflows()) {
|
| 117 |
+
tooltip = null;
|
| 118 |
+
return;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
tooltip = tooltipPosition(event);
|
| 122 |
+
|
| 123 |
+
tick().then(() => {
|
| 124 |
+
if (!tooltip) return;
|
| 125 |
+
tooltip = tooltipPosition(event);
|
| 126 |
+
});
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
function hideTooltip() {
|
| 130 |
+
tooltip = null;
|
| 131 |
+
}
|
| 132 |
+
</script>
|
| 133 |
+
|
| 134 |
+
<span
|
| 135 |
+
bind:this={labelElement}
|
| 136 |
+
class={labelClass}
|
| 137 |
+
style={labelStyle}
|
| 138 |
+
{role}
|
| 139 |
+
aria-label={ariaLabel ?? text}
|
| 140 |
+
onpointerenter={updateTooltip}
|
| 141 |
+
onpointermove={updateTooltip}
|
| 142 |
+
onpointerleave={hideTooltip}
|
| 143 |
+
>
|
| 144 |
+
{#if children}
|
| 145 |
+
{@render children()}
|
| 146 |
+
{:else}
|
| 147 |
+
{text}
|
| 148 |
+
{/if}
|
| 149 |
+
</span>
|
| 150 |
+
|
| 151 |
+
{#if tooltip}
|
| 152 |
+
<CursorTooltip
|
| 153 |
+
bind:ref={tooltipElement}
|
| 154 |
+
x={tooltip.x}
|
| 155 |
+
y={tooltip.y}
|
| 156 |
+
title={text}
|
| 157 |
+
class={tooltipClass}
|
| 158 |
+
/>
|
| 159 |
+
{/if}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
| 6 |
BenchmarkRankingRow,
|
| 7 |
DetailMetric,
|
| 8 |
DetailsLanguageFilter,
|
|
|
|
| 9 |
MatrixDataset,
|
| 10 |
MatrixSortDirection,
|
| 11 |
ModelGroupRow,
|
|
@@ -88,6 +89,14 @@ function modelKey(model: GuardModel) {
|
|
| 88 |
return model.modelId ?? model.name;
|
| 89 |
}
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
export function matrixCellValue(
|
| 92 |
model: GuardModel,
|
| 93 |
dataset: MatrixDataset,
|
|
@@ -263,6 +272,85 @@ export function filterModels(
|
|
| 263 |
.sort((a, b) => modelSortValue(b, sort) - modelSortValue(a, sort));
|
| 264 |
}
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
function groupName(key: string, dataset: DetailsDataset, locale: SupportedLocale) {
|
| 267 |
return localized(dataset.groupLabels[key], locale, key);
|
| 268 |
}
|
|
|
|
| 6 |
BenchmarkRankingRow,
|
| 7 |
DetailMetric,
|
| 8 |
DetailsLanguageFilter,
|
| 9 |
+
MatrixGroup,
|
| 10 |
MatrixDataset,
|
| 11 |
MatrixSortDirection,
|
| 12 |
ModelGroupRow,
|
|
|
|
| 89 |
return model.modelId ?? model.name;
|
| 90 |
}
|
| 91 |
|
| 92 |
+
function csvCell(value: string | number | null | undefined) {
|
| 93 |
+
return `"${String(value ?? '').replaceAll('"', '""')}"`;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
function csvNumber(value: number | null | undefined, digits = 4) {
|
| 97 |
+
return isNumber(value) ? value.toFixed(digits) : '';
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
export function matrixCellValue(
|
| 101 |
model: GuardModel,
|
| 102 |
dataset: MatrixDataset,
|
|
|
|
| 272 |
.sort((a, b) => modelSortValue(b, sort) - modelSortValue(a, sort));
|
| 273 |
}
|
| 274 |
|
| 275 |
+
export function detailMatrixRowsToCsv(
|
| 276 |
+
rows: readonly GuardModel[],
|
| 277 |
+
groups: readonly MatrixGroup[],
|
| 278 |
+
metric: DetailMetric,
|
| 279 |
+
datasetCells: Record<string, Record<string, DetailsDatasetMatrixCell>>
|
| 280 |
+
) {
|
| 281 |
+
const datasets = groups.flatMap((group) =>
|
| 282 |
+
group.datasets.map((dataset) => ({
|
| 283 |
+
...dataset,
|
| 284 |
+
header: `${group.label} / ${dataset.label} ${metric}`
|
| 285 |
+
}))
|
| 286 |
+
);
|
| 287 |
+
const header = [
|
| 288 |
+
'rank',
|
| 289 |
+
'model',
|
| 290 |
+
'organization',
|
| 291 |
+
'size',
|
| 292 |
+
'integral',
|
| 293 |
+
'metric',
|
| 294 |
+
...datasets.map((dataset) => dataset.header)
|
| 295 |
+
];
|
| 296 |
+
const lines = rows.map((model, index) => [
|
| 297 |
+
index + 1,
|
| 298 |
+
model.name,
|
| 299 |
+
model.org,
|
| 300 |
+
model.params,
|
| 301 |
+
csvNumber(model.integral),
|
| 302 |
+
metric,
|
| 303 |
+
...datasets.map((dataset) =>
|
| 304 |
+
csvNumber(detailMetricValue(datasetCells[modelKey(model)]?.[dataset.id], metric))
|
| 305 |
+
)
|
| 306 |
+
]);
|
| 307 |
+
|
| 308 |
+
return [header, ...lines].map((row) => row.map(csvCell).join(',')).join('\n');
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
export function detailModelRowsToCsv(rows: readonly GuardModel[]) {
|
| 312 |
+
const header = [
|
| 313 |
+
'rank',
|
| 314 |
+
'model',
|
| 315 |
+
'organization',
|
| 316 |
+
'family',
|
| 317 |
+
'type',
|
| 318 |
+
'license',
|
| 319 |
+
'size',
|
| 320 |
+
'languages',
|
| 321 |
+
'integral',
|
| 322 |
+
'min_group',
|
| 323 |
+
'overall_fpr',
|
| 324 |
+
'overall_fnr',
|
| 325 |
+
'overall_f1',
|
| 326 |
+
'p50_ms',
|
| 327 |
+
'p95_ms',
|
| 328 |
+
'p99_ms',
|
| 329 |
+
'run_date'
|
| 330 |
+
];
|
| 331 |
+
const lines = rows.map((model, index) => [
|
| 332 |
+
index + 1,
|
| 333 |
+
model.name,
|
| 334 |
+
model.org,
|
| 335 |
+
model.family,
|
| 336 |
+
model.type,
|
| 337 |
+
model.license,
|
| 338 |
+
model.params,
|
| 339 |
+
model.languages.join('; '),
|
| 340 |
+
csvNumber(model.integral),
|
| 341 |
+
csvNumber(model.minGroup),
|
| 342 |
+
csvNumber(model.fpr),
|
| 343 |
+
csvNumber(model.fnr),
|
| 344 |
+
csvNumber(model.f1),
|
| 345 |
+
csvNumber(model.p50, 1),
|
| 346 |
+
csvNumber(model.p95, 1),
|
| 347 |
+
csvNumber(model.p99, 1),
|
| 348 |
+
model.runDate
|
| 349 |
+
]);
|
| 350 |
+
|
| 351 |
+
return [header, ...lines].map((row) => row.map(csvCell).join(',')).join('\n');
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
function groupName(key: string, dataset: DetailsDataset, locale: SupportedLocale) {
|
| 355 |
return localized(dataset.groupLabels[key], locale, key);
|
| 356 |
}
|
|
@@ -160,6 +160,9 @@ export function adaptBucketTools(snapshot: BucketVisualizationSnapshot): ToolsDa
|
|
| 160 |
fnrReal: row.fnr_real,
|
| 161 |
fnrRobust: row.fnr_robust,
|
| 162 |
deltaFnr: row.delta_fnr,
|
|
|
|
|
|
|
|
|
|
| 163 |
categoryLabel: row.category_label
|
| 164 |
}))
|
| 165 |
}
|
|
|
|
| 160 |
fnrReal: row.fnr_real,
|
| 161 |
fnrRobust: row.fnr_robust,
|
| 162 |
deltaFnr: row.delta_fnr,
|
| 163 |
+
fprReal: row.fpr_real,
|
| 164 |
+
fprRobust: row.fpr_robust,
|
| 165 |
+
deltaFpr: row.delta_fpr,
|
| 166 |
categoryLabel: row.category_label
|
| 167 |
}))
|
| 168 |
}
|
|
@@ -280,6 +280,9 @@ export type BucketRobustnessRow = {
|
|
| 280 |
fnr_real: NullableMetricValue;
|
| 281 |
fnr_robust: NullableMetricValue;
|
| 282 |
delta_fnr: NullableMetricValue;
|
|
|
|
|
|
|
|
|
|
| 283 |
category_label: LocalizedString;
|
| 284 |
};
|
| 285 |
|
|
|
|
| 280 |
fnr_real: NullableMetricValue;
|
| 281 |
fnr_robust: NullableMetricValue;
|
| 282 |
delta_fnr: NullableMetricValue;
|
| 283 |
+
fpr_real: NullableMetricValue;
|
| 284 |
+
fpr_robust: NullableMetricValue;
|
| 285 |
+
delta_fpr: NullableMetricValue;
|
| 286 |
category_label: LocalizedString;
|
| 287 |
};
|
| 288 |
|
|
@@ -227,6 +227,9 @@ function parseRobustnessRow(value: unknown, index: number): BucketRobustnessRow
|
|
| 227 |
fnr_real: nullableNumber(item.fnr_real, `${field}.fnr_real`),
|
| 228 |
fnr_robust: nullableNumber(item.fnr_robust, `${field}.fnr_robust`),
|
| 229 |
delta_fnr: nullableNumber(item.delta_fnr, `${field}.delta_fnr`),
|
|
|
|
|
|
|
|
|
|
| 230 |
category_label: localized(item.category_label, `${field}.category_label`)
|
| 231 |
};
|
| 232 |
}
|
|
|
|
| 227 |
fnr_real: nullableNumber(item.fnr_real, `${field}.fnr_real`),
|
| 228 |
fnr_robust: nullableNumber(item.fnr_robust, `${field}.fnr_robust`),
|
| 229 |
delta_fnr: nullableNumber(item.delta_fnr, `${field}.delta_fnr`),
|
| 230 |
+
fpr_real: nullableNumber(item.fpr_real ?? null, `${field}.fpr_real`),
|
| 231 |
+
fpr_robust: nullableNumber(item.fpr_robust ?? null, `${field}.fpr_robust`),
|
| 232 |
+
delta_fpr: nullableNumber(item.delta_fpr ?? null, `${field}.delta_fpr`),
|
| 233 |
category_label: localized(item.category_label, `${field}.category_label`)
|
| 234 |
};
|
| 235 |
}
|
|
@@ -105,6 +105,9 @@ export type ToolsRobustnessVisualization = {
|
|
| 105 |
fnrReal: number | null;
|
| 106 |
fnrRobust: number | null;
|
| 107 |
deltaFnr: number | null;
|
|
|
|
|
|
|
|
|
|
| 108 |
categoryLabel: LocalizedString;
|
| 109 |
}>;
|
| 110 |
};
|
|
|
|
| 105 |
fnrReal: number | null;
|
| 106 |
fnrRobust: number | null;
|
| 107 |
deltaFnr: number | null;
|
| 108 |
+
fprReal: number | null;
|
| 109 |
+
fprRobust: number | null;
|
| 110 |
+
deltaFpr: number | null;
|
| 111 |
categoryLabel: LocalizedString;
|
| 112 |
}>;
|
| 113 |
};
|
|
@@ -4,15 +4,3 @@ export type ToolsRadarScale = 'score' | 'f1';
|
|
| 4 |
export type ToolsHeatmapMetric = ToolsVizMetric | 'score';
|
| 5 |
export type ToolsSortDirection = 'asc' | 'desc';
|
| 6 |
export type ToolsLatencySortKey = 'model' | 'p50' | 'p95' | 'p99' | 'errorRate';
|
| 7 |
-
|
| 8 |
-
export type ToolsTooltipRow = {
|
| 9 |
-
label: string;
|
| 10 |
-
value: string;
|
| 11 |
-
};
|
| 12 |
-
|
| 13 |
-
export type ToolsChartTooltip = {
|
| 14 |
-
x: number;
|
| 15 |
-
y: number;
|
| 16 |
-
title: string;
|
| 17 |
-
rows: ToolsTooltipRow[];
|
| 18 |
-
};
|
|
|
|
| 4 |
export type ToolsHeatmapMetric = ToolsVizMetric | 'score';
|
| 5 |
export type ToolsSortDirection = 'asc' | 'desc';
|
| 6 |
export type ToolsLatencySortKey = 'model' | 'p50' | 'p95' | 'p99' | 'errorRate';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,7 +1,14 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import BucketDataFallback from '$lib/components/BucketDataFallback.svelte';
|
| 4 |
import BucketDataWarning from '$lib/components/BucketDataWarning.svelte';
|
|
|
|
| 5 |
import Select from '$lib/components/Select.svelte';
|
| 6 |
import Sheet from '$lib/components/Sheet.svelte';
|
| 7 |
import Tabs from '$lib/components/Tabs.svelte';
|
|
@@ -153,8 +160,17 @@
|
|
| 153 |
key: column.key,
|
| 154 |
label: column.label,
|
| 155 |
metric: column,
|
| 156 |
-
headClass:
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
ariaSort: () =>
|
| 159 |
sortKey === column.key ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'
|
| 160 |
}))
|
|
@@ -526,7 +542,7 @@
|
|
| 526 |
{@const model = modelByFilterValue(option.value)}
|
| 527 |
<span class="min-w-0 truncate">{model?.short ?? option.label}</span>
|
| 528 |
{#if model}
|
| 529 |
-
<code class="ml-auto text-xs text-muted-foreground">{model.
|
| 530 |
{/if}
|
| 531 |
{/if}
|
| 532 |
{/snippet}
|
|
@@ -585,11 +601,13 @@
|
|
| 585 |
{#snippet SortHeader({ column }: { column: LeaderboardTableColumn })}
|
| 586 |
{@const metric = column.metric}
|
| 587 |
{#if metric}
|
|
|
|
| 588 |
<button
|
| 589 |
type="button"
|
| 590 |
class={cn(
|
| 591 |
'inline-flex w-full justify-start flex-col gap-0.5 text-sm font-medium text-inherit transition-colors hover:text-inherit',
|
| 592 |
-
metric.align === 'right' ? 'items-end text-right' : 'items-start text-left'
|
|
|
|
| 593 |
)}
|
| 594 |
onclick={() => updateSort(metric.key)}
|
| 595 |
>
|
|
@@ -600,7 +618,15 @@
|
|
| 600 |
)}
|
| 601 |
>
|
| 602 |
{metric.label()}
|
| 603 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
</span>
|
| 605 |
{#if metric.directionLabel}
|
| 606 |
<span class="text-[10px] leading-3 font-light text-muted-foreground normal-case">
|
|
@@ -620,9 +646,14 @@
|
|
| 620 |
{row.model.org.slice(0, 2)}
|
| 621 |
</div>
|
| 622 |
<div class="min-w-0 space-y-1">
|
| 623 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
<p class="truncate text-xs text-muted-foreground">
|
| 625 |
-
{row.model.family} · {row.
|
| 626 |
</p>
|
| 627 |
{#if row.model.leak}
|
| 628 |
<Badge
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import {
|
| 3 |
+
ArrowDownIcon,
|
| 4 |
+
ArrowUpDownIcon,
|
| 5 |
+
ArrowUpIcon,
|
| 6 |
+
DownloadIcon,
|
| 7 |
+
ExternalLinkIcon
|
| 8 |
+
} from '@lucide/svelte';
|
| 9 |
import BucketDataFallback from '$lib/components/BucketDataFallback.svelte';
|
| 10 |
import BucketDataWarning from '$lib/components/BucketDataWarning.svelte';
|
| 11 |
+
import OverflowTooltip from '$lib/components/OverflowTooltip.svelte';
|
| 12 |
import Select from '$lib/components/Select.svelte';
|
| 13 |
import Sheet from '$lib/components/Sheet.svelte';
|
| 14 |
import Tabs from '$lib/components/Tabs.svelte';
|
|
|
|
| 160 |
key: column.key,
|
| 161 |
label: column.label,
|
| 162 |
metric: column,
|
| 163 |
+
headClass: () =>
|
| 164 |
+
cn(
|
| 165 |
+
'align-top transition-colors',
|
| 166 |
+
column.align === 'right' && 'text-right',
|
| 167 |
+
sortKey === column.key && 'bg-muted/80 text-foreground'
|
| 168 |
+
),
|
| 169 |
+
cellClass: () =>
|
| 170 |
+
cn(
|
| 171 |
+
column.align === 'right' && 'text-right',
|
| 172 |
+
sortKey === column.key && 'bg-muted/20 text-foreground'
|
| 173 |
+
),
|
| 174 |
ariaSort: () =>
|
| 175 |
sortKey === column.key ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'
|
| 176 |
}))
|
|
|
|
| 542 |
{@const model = modelByFilterValue(option.value)}
|
| 543 |
<span class="min-w-0 truncate">{model?.short ?? option.label}</span>
|
| 544 |
{#if model}
|
| 545 |
+
<code class="ml-auto text-xs text-muted-foreground">{model.params}</code>
|
| 546 |
{/if}
|
| 547 |
{/if}
|
| 548 |
{/snippet}
|
|
|
|
| 601 |
{#snippet SortHeader({ column }: { column: LeaderboardTableColumn })}
|
| 602 |
{@const metric = column.metric}
|
| 603 |
{#if metric}
|
| 604 |
+
{@const isActive = sortKey === metric.key}
|
| 605 |
<button
|
| 606 |
type="button"
|
| 607 |
class={cn(
|
| 608 |
'inline-flex w-full justify-start flex-col gap-0.5 text-sm font-medium text-inherit transition-colors hover:text-inherit',
|
| 609 |
+
metric.align === 'right' ? 'items-end text-right' : 'items-start text-left',
|
| 610 |
+
isActive && 'text-foreground'
|
| 611 |
)}
|
| 612 |
onclick={() => updateSort(metric.key)}
|
| 613 |
>
|
|
|
|
| 618 |
)}
|
| 619 |
>
|
| 620 |
{metric.label()}
|
| 621 |
+
{#if isActive}
|
| 622 |
+
{#if sortDirection === 'asc'}
|
| 623 |
+
<ArrowUpIcon class="size-3.5 shrink-0" />
|
| 624 |
+
{:else}
|
| 625 |
+
<ArrowDownIcon class="size-3.5 shrink-0" />
|
| 626 |
+
{/if}
|
| 627 |
+
{:else}
|
| 628 |
+
<ArrowUpDownIcon class="size-3.5 shrink-0 text-muted-foreground" />
|
| 629 |
+
{/if}
|
| 630 |
</span>
|
| 631 |
{#if metric.directionLabel}
|
| 632 |
<span class="text-[10px] leading-3 font-light text-muted-foreground normal-case">
|
|
|
|
| 646 |
{row.model.org.slice(0, 2)}
|
| 647 |
</div>
|
| 648 |
<div class="min-w-0 space-y-1">
|
| 649 |
+
<OverflowTooltip
|
| 650 |
+
text={row.model.short}
|
| 651 |
+
class="block w-full min-w-0 truncate text-sm font-semibold text-foreground"
|
| 652 |
+
triggerClass="block w-full min-w-0"
|
| 653 |
+
contentClass="max-w-sm break-all"
|
| 654 |
+
/>
|
| 655 |
<p class="truncate text-xs text-muted-foreground">
|
| 656 |
+
{row.model.family} · {row.model.params} · {row.model.org}
|
| 657 |
</p>
|
| 658 |
{#if row.model.leak}
|
| 659 |
<Badge
|
|
@@ -1,7 +1,14 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import BucketDataFallback from '$lib/components/BucketDataFallback.svelte';
|
| 4 |
import BucketDataWarning from '$lib/components/BucketDataWarning.svelte';
|
|
|
|
| 5 |
import OverflowTooltip from '$lib/components/OverflowTooltip.svelte';
|
| 6 |
import PageHeadingSection from '$lib/components/PageHeadingSection.svelte';
|
| 7 |
import Select from '$lib/components/Select.svelte';
|
|
@@ -16,6 +23,8 @@
|
|
| 16 |
buildBenchmarkCards,
|
| 17 |
buildMatrixGroups,
|
| 18 |
buildMatrixRows,
|
|
|
|
|
|
|
| 19 |
filterModels,
|
| 20 |
matrixCellValue,
|
| 21 |
matrixDatasetLabel,
|
|
@@ -49,6 +58,9 @@
|
|
| 49 |
type ColumnExtrema = { best: number | null; worst: number | null };
|
| 50 |
|
| 51 |
const selectContentClass = 'max-h-72';
|
|
|
|
|
|
|
|
|
|
| 52 |
let { data }: PageProps = $props();
|
| 53 |
const detailsState = $derived(data.detailsState);
|
| 54 |
const details = $derived(detailsState.data);
|
|
@@ -188,6 +200,19 @@
|
|
| 188 |
const filteredModels = $derived.by(() =>
|
| 189 |
filterModels(modelSearch, modelLanguage, modelType, modelSize, modelSort, models)
|
| 190 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
const selectedModelGroups = $derived.by(() =>
|
| 192 |
modelGroupRows(selectedModel, details, getLocale())
|
| 193 |
);
|
|
@@ -285,9 +310,17 @@
|
|
| 285 |
return matrixSortDirection === -1 ? 'descending' : 'ascending';
|
| 286 |
}
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
function metricCellClass(value: number | null, metric: DetailMetric, datasetName: string) {
|
| 289 |
return cn(
|
| 290 |
-
'matrix-cell min-w-32 border-b border-l px-3 py-3 text-
|
| 291 |
!isNumber(value) && 'matrix-cell-empty !bg-muted/30 text-muted-foreground',
|
| 292 |
isNumber(value) &&
|
| 293 |
extremaTone(roundedDetailMetric(value, metric), matrixColumnExtrema[datasetName])
|
|
@@ -300,7 +333,11 @@
|
|
| 300 |
datasetName: string,
|
| 301 |
startsGroup: boolean
|
| 302 |
) {
|
| 303 |
-
return cn(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
}
|
| 305 |
|
| 306 |
function scorePillClass(value: number | null) {
|
|
@@ -509,18 +546,15 @@
|
|
| 509 |
<Button variant="outline" size="sm" onclick={() => (selectedGroups = [])}>
|
| 510 |
{m.details_clear()}
|
| 511 |
</Button>
|
| 512 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
<DownloadIcon data-icon="inline-start" />
|
| 514 |
{m.ranking_csv()}
|
| 515 |
</Button>
|
| 516 |
-
<Button size="sm" class="bg-[#0dce97] text-emerald-950 hover:bg-[#0dce97]/90">
|
| 517 |
-
<DownloadIcon data-icon="inline-start" />
|
| 518 |
-
Excel
|
| 519 |
-
</Button>
|
| 520 |
-
<Button size="sm" class="bg-[#0dce97] text-emerald-950 hover:bg-[#0dce97]/90">
|
| 521 |
-
<DownloadIcon data-icon="inline-start" />
|
| 522 |
-
PDF
|
| 523 |
-
</Button>
|
| 524 |
</div>
|
| 525 |
</section>
|
| 526 |
|
|
@@ -542,7 +576,10 @@
|
|
| 542 |
<thead>
|
| 543 |
<tr>
|
| 544 |
<th
|
| 545 |
-
class=
|
|
|
|
|
|
|
|
|
|
| 546 |
rowspan="2"
|
| 547 |
scope="col"
|
| 548 |
aria-sort={matrixAriaSort(null)}
|
|
@@ -550,11 +587,22 @@
|
|
| 550 |
<Button
|
| 551 |
variant="ghost"
|
| 552 |
size="sm"
|
| 553 |
-
class=
|
|
|
|
|
|
|
|
|
|
| 554 |
onclick={() => handleMatrixSort(null)}
|
| 555 |
>
|
| 556 |
{m.details_matrix_model_sort()}
|
| 557 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
</Button>
|
| 559 |
</th>
|
| 560 |
{#each visibleMatrixGroups as group, groupIndex (group.key)}
|
|
@@ -576,6 +624,7 @@
|
|
| 576 |
<th
|
| 577 |
class={cn(
|
| 578 |
'sticky top-12 z-20 h-12 min-w-32 border-b border-l border-border bg-muted px-3 py-3 text-right text-xs font-semibold whitespace-nowrap text-muted-foreground',
|
|
|
|
| 579 |
groupIndex === 0 && datasetIndex === 0 && 'border-l-0',
|
| 580 |
(groupIndex > 0 || datasetIndex > 0) &&
|
| 581 |
datasetIndex === 0 &&
|
|
@@ -587,12 +636,27 @@
|
|
| 587 |
<Button
|
| 588 |
variant="ghost"
|
| 589 |
size="sm"
|
| 590 |
-
class=
|
| 591 |
-
|
|
|
|
|
|
|
| 592 |
onclick={() => handleMatrixSort(dataset.name)}
|
| 593 |
>
|
| 594 |
-
|
| 595 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
</Button>
|
| 597 |
</th>
|
| 598 |
{/each}
|
|
@@ -605,7 +669,10 @@
|
|
| 605 |
class="hover:[&>td.matrix-cell-bad]:!bg-red-500/15 hover:[&>td.matrix-cell-empty]:!bg-muted/40 hover:[&>td.matrix-cell-good]:!bg-emerald-500/15 hover:[&>th]:bg-muted"
|
| 606 |
>
|
| 607 |
<th
|
| 608 |
-
class=
|
|
|
|
|
|
|
|
|
|
| 609 |
scope="row"
|
| 610 |
>
|
| 611 |
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
|
|
@@ -990,7 +1057,12 @@
|
|
| 990 |
>
|
| 991 |
{m.details_model_count({ shown: filteredModels.length, total: models.length })}
|
| 992 |
</span>
|
| 993 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 994 |
<DownloadIcon data-icon="inline-start" />
|
| 995 |
{m.ranking_csv()}
|
| 996 |
</Button>
|
|
@@ -1182,51 +1254,68 @@
|
|
| 1182 |
</h3>
|
| 1183 |
{#if selectedModelRadarPoints.length >= 3}
|
| 1184 |
<div class="flex justify-center">
|
| 1185 |
-
<
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1192 |
<polygon
|
| 1193 |
-
points={
|
| 1194 |
-
fill="
|
| 1195 |
-
stroke="
|
| 1196 |
-
stroke-width="
|
| 1197 |
-
stroke-dasharray="3 4"
|
| 1198 |
-
class="text-border"
|
| 1199 |
/>
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
| 1224 |
-
|
|
|
|
| 1225 |
/>
|
| 1226 |
-
|
| 1227 |
-
<circle cx={point.x} cy={point.y} r="3" fill="#0dce97" />
|
| 1228 |
-
{/each}
|
| 1229 |
-
</svg>
|
| 1230 |
</div>
|
| 1231 |
<div
|
| 1232 |
class="flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-xs text-muted-foreground"
|
|
@@ -1334,9 +1423,12 @@
|
|
| 1334 |
<div class="space-y-3">
|
| 1335 |
{#each selectedModelGroups as group (group.name)}
|
| 1336 |
<div class="grid items-center gap-3 sm:grid-cols-[minmax(9rem,13rem)_1fr_auto]">
|
| 1337 |
-
<
|
| 1338 |
-
{group.name}
|
| 1339 |
-
|
|
|
|
|
|
|
|
|
|
| 1340 |
<div class="h-2 rounded-full bg-muted">
|
| 1341 |
<div
|
| 1342 |
class="h-full rounded-full bg-[#0dce97]"
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import {
|
| 3 |
+
ArrowDownIcon,
|
| 4 |
+
ArrowUpDownIcon,
|
| 5 |
+
ArrowUpIcon,
|
| 6 |
+
DownloadIcon,
|
| 7 |
+
ExternalLinkIcon
|
| 8 |
+
} from '@lucide/svelte';
|
| 9 |
import BucketDataFallback from '$lib/components/BucketDataFallback.svelte';
|
| 10 |
import BucketDataWarning from '$lib/components/BucketDataWarning.svelte';
|
| 11 |
+
import RadarLabelsOverlay from '$lib/components/charts/RadarLabelsOverlay.svelte';
|
| 12 |
import OverflowTooltip from '$lib/components/OverflowTooltip.svelte';
|
| 13 |
import PageHeadingSection from '$lib/components/PageHeadingSection.svelte';
|
| 14 |
import Select from '$lib/components/Select.svelte';
|
|
|
|
| 23 |
buildBenchmarkCards,
|
| 24 |
buildMatrixGroups,
|
| 25 |
buildMatrixRows,
|
| 26 |
+
detailMatrixRowsToCsv,
|
| 27 |
+
detailModelRowsToCsv,
|
| 28 |
filterModels,
|
| 29 |
matrixCellValue,
|
| 30 |
matrixDatasetLabel,
|
|
|
|
| 58 |
type ColumnExtrema = { best: number | null; worst: number | null };
|
| 59 |
|
| 60 |
const selectContentClass = 'max-h-72';
|
| 61 |
+
const modelSheetRadarViewBoxSize = 320;
|
| 62 |
+
const modelSheetRadarLabelRadius = 132;
|
| 63 |
+
const modelSheetRadarHorizontalLabelScale = 0.86;
|
| 64 |
let { data }: PageProps = $props();
|
| 65 |
const detailsState = $derived(data.detailsState);
|
| 66 |
const details = $derived(detailsState.data);
|
|
|
|
| 200 |
const filteredModels = $derived.by(() =>
|
| 201 |
filterModels(modelSearch, modelLanguage, modelType, modelSize, modelSort, models)
|
| 202 |
);
|
| 203 |
+
const matrixCsvHref = $derived(
|
| 204 |
+
`data:text/csv;charset=utf-8,${encodeURIComponent(
|
| 205 |
+
detailMatrixRowsToCsv(
|
| 206 |
+
matrixRows,
|
| 207 |
+
visibleMatrixGroups,
|
| 208 |
+
benchmarkMetric,
|
| 209 |
+
details.datasetMatrix.cellsByModelId
|
| 210 |
+
)
|
| 211 |
+
)}`
|
| 212 |
+
);
|
| 213 |
+
const modelsCsvHref = $derived(
|
| 214 |
+
`data:text/csv;charset=utf-8,${encodeURIComponent(detailModelRowsToCsv(filteredModels))}`
|
| 215 |
+
);
|
| 216 |
const selectedModelGroups = $derived.by(() =>
|
| 217 |
modelGroupRows(selectedModel, details, getLocale())
|
| 218 |
);
|
|
|
|
| 310 |
return matrixSortDirection === -1 ? 'descending' : 'ascending';
|
| 311 |
}
|
| 312 |
|
| 313 |
+
function matrixSortIsActive(datasetName: string | null) {
|
| 314 |
+
return matrixSortDataset === datasetName;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
function matrixSortColumnClass(datasetName: string | null) {
|
| 318 |
+
return matrixSortIsActive(datasetName) && 'bg-muted/80 text-foreground';
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
function metricCellClass(value: number | null, metric: DetailMetric, datasetName: string) {
|
| 322 |
return cn(
|
| 323 |
+
'matrix-cell min-w-32 border-b border-l px-3 py-3 text-center align-middle font-mono text-base font-semibold tabular-nums',
|
| 324 |
!isNumber(value) && 'matrix-cell-empty !bg-muted/30 text-muted-foreground',
|
| 325 |
isNumber(value) &&
|
| 326 |
extremaTone(roundedDetailMetric(value, metric), matrixColumnExtrema[datasetName])
|
|
|
|
| 333 |
datasetName: string,
|
| 334 |
startsGroup: boolean
|
| 335 |
) {
|
| 336 |
+
return cn(
|
| 337 |
+
metricCellClass(value, metric, datasetName),
|
| 338 |
+
startsGroup && 'border-l-border',
|
| 339 |
+
matrixSortIsActive(datasetName) && 'bg-muted/20 text-foreground'
|
| 340 |
+
);
|
| 341 |
}
|
| 342 |
|
| 343 |
function scorePillClass(value: number | null) {
|
|
|
|
| 546 |
<Button variant="outline" size="sm" onclick={() => (selectedGroups = [])}>
|
| 547 |
{m.details_clear()}
|
| 548 |
</Button>
|
| 549 |
+
<Button
|
| 550 |
+
href={matrixCsvHref}
|
| 551 |
+
download={`hivetrace_details_matrix_${benchmarkMetric}.csv`}
|
| 552 |
+
size="sm"
|
| 553 |
+
class="ml-auto bg-[#0dce97] text-emerald-950 hover:bg-[#0dce97]/90"
|
| 554 |
+
>
|
| 555 |
<DownloadIcon data-icon="inline-start" />
|
| 556 |
{m.ranking_csv()}
|
| 557 |
</Button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 558 |
</div>
|
| 559 |
</section>
|
| 560 |
|
|
|
|
| 576 |
<thead>
|
| 577 |
<tr>
|
| 578 |
<th
|
| 579 |
+
class={cn(
|
| 580 |
+
'sticky top-0 z-30 h-24 min-w-80 max-w-80 border-r border-b border-border bg-muted px-4 py-3 text-left align-middle md:left-0',
|
| 581 |
+
matrixSortColumnClass(null)
|
| 582 |
+
)}
|
| 583 |
rowspan="2"
|
| 584 |
scope="col"
|
| 585 |
aria-sort={matrixAriaSort(null)}
|
|
|
|
| 587 |
<Button
|
| 588 |
variant="ghost"
|
| 589 |
size="sm"
|
| 590 |
+
class={cn(
|
| 591 |
+
'h-auto justify-start gap-1.5 p-0 text-xs font-medium hover:bg-transparent',
|
| 592 |
+
matrixSortIsActive(null) && 'text-foreground'
|
| 593 |
+
)}
|
| 594 |
onclick={() => handleMatrixSort(null)}
|
| 595 |
>
|
| 596 |
{m.details_matrix_model_sort()}
|
| 597 |
+
{#if matrixSortIsActive(null)}
|
| 598 |
+
{#if matrixSortDirection === 1}
|
| 599 |
+
<ArrowUpIcon class="size-3.5 shrink-0" />
|
| 600 |
+
{:else}
|
| 601 |
+
<ArrowDownIcon class="size-3.5 shrink-0" />
|
| 602 |
+
{/if}
|
| 603 |
+
{:else}
|
| 604 |
+
<ArrowUpDownIcon class="size-3.5 shrink-0 text-muted-foreground" />
|
| 605 |
+
{/if}
|
| 606 |
</Button>
|
| 607 |
</th>
|
| 608 |
{#each visibleMatrixGroups as group, groupIndex (group.key)}
|
|
|
|
| 624 |
<th
|
| 625 |
class={cn(
|
| 626 |
'sticky top-12 z-20 h-12 min-w-32 border-b border-l border-border bg-muted px-3 py-3 text-right text-xs font-semibold whitespace-nowrap text-muted-foreground',
|
| 627 |
+
matrixSortColumnClass(dataset.name),
|
| 628 |
groupIndex === 0 && datasetIndex === 0 && 'border-l-0',
|
| 629 |
(groupIndex > 0 || datasetIndex > 0) &&
|
| 630 |
datasetIndex === 0 &&
|
|
|
|
| 636 |
<Button
|
| 637 |
variant="ghost"
|
| 638 |
size="sm"
|
| 639 |
+
class={cn(
|
| 640 |
+
'h-auto justify-end gap-1.5 p-0 text-[11px] font-medium text-muted-foreground hover:bg-transparent hover:text-foreground',
|
| 641 |
+
matrixSortIsActive(dataset.name) && 'text-foreground'
|
| 642 |
+
)}
|
| 643 |
onclick={() => handleMatrixSort(dataset.name)}
|
| 644 |
>
|
| 645 |
+
<OverflowTooltip
|
| 646 |
+
text={matrixDatasetLabel(dataset)}
|
| 647 |
+
class="block max-w-28 truncate"
|
| 648 |
+
triggerClass="block min-w-0"
|
| 649 |
+
contentClass="max-w-sm break-all"
|
| 650 |
+
/>
|
| 651 |
+
{#if matrixSortIsActive(dataset.name)}
|
| 652 |
+
{#if matrixSortDirection === 1}
|
| 653 |
+
<ArrowUpIcon class="size-3.5 shrink-0" />
|
| 654 |
+
{:else}
|
| 655 |
+
<ArrowDownIcon class="size-3.5 shrink-0" />
|
| 656 |
+
{/if}
|
| 657 |
+
{:else}
|
| 658 |
+
<ArrowUpDownIcon class="size-3.5 shrink-0 text-muted-foreground" />
|
| 659 |
+
{/if}
|
| 660 |
</Button>
|
| 661 |
</th>
|
| 662 |
{/each}
|
|
|
|
| 669 |
class="hover:[&>td.matrix-cell-bad]:!bg-red-500/15 hover:[&>td.matrix-cell-empty]:!bg-muted/40 hover:[&>td.matrix-cell-good]:!bg-emerald-500/15 hover:[&>th]:bg-muted"
|
| 670 |
>
|
| 671 |
<th
|
| 672 |
+
class={cn(
|
| 673 |
+
'z-20 min-w-80 max-w-80 border-r border-b bg-card px-4 py-3 text-left md:sticky md:left-0',
|
| 674 |
+
matrixSortIsActive(null) && 'bg-muted/30'
|
| 675 |
+
)}
|
| 676 |
scope="row"
|
| 677 |
>
|
| 678 |
<div class="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3">
|
|
|
|
| 1057 |
>
|
| 1058 |
{m.details_model_count({ shown: filteredModels.length, total: models.length })}
|
| 1059 |
</span>
|
| 1060 |
+
<Button
|
| 1061 |
+
href={modelsCsvHref}
|
| 1062 |
+
download="hivetrace_details_models.csv"
|
| 1063 |
+
size="sm"
|
| 1064 |
+
class="bg-[#0dce97] text-emerald-950 hover:bg-[#0dce97]/90"
|
| 1065 |
+
>
|
| 1066 |
<DownloadIcon data-icon="inline-start" />
|
| 1067 |
{m.ranking_csv()}
|
| 1068 |
</Button>
|
|
|
|
| 1254 |
</h3>
|
| 1255 |
{#if selectedModelRadarPoints.length >= 3}
|
| 1256 |
<div class="flex justify-center">
|
| 1257 |
+
<div class="relative h-[28rem] w-full max-w-xl">
|
| 1258 |
+
<svg
|
| 1259 |
+
viewBox="0 0 320 320"
|
| 1260 |
+
class="h-full w-full"
|
| 1261 |
+
role="img"
|
| 1262 |
+
aria-label={m.details_model_sheet_radar()}
|
| 1263 |
+
>
|
| 1264 |
+
{#each radarGridPolygons as polygon (polygon)}
|
| 1265 |
+
<polygon
|
| 1266 |
+
points={polygon}
|
| 1267 |
+
fill="none"
|
| 1268 |
+
stroke="currentColor"
|
| 1269 |
+
stroke-width="1"
|
| 1270 |
+
stroke-dasharray="3 4"
|
| 1271 |
+
class="text-border"
|
| 1272 |
+
/>
|
| 1273 |
+
{/each}
|
| 1274 |
+
{#each selectedModelRadarPoints as point, index (point.name)}
|
| 1275 |
+
<line
|
| 1276 |
+
x1="160"
|
| 1277 |
+
y1="160"
|
| 1278 |
+
x2={radarPoint(index, selectedModelRadarPoints.length, 118)[0]}
|
| 1279 |
+
y2={radarPoint(index, selectedModelRadarPoints.length, 118)[1]}
|
| 1280 |
+
stroke="currentColor"
|
| 1281 |
+
stroke-width="1"
|
| 1282 |
+
class="text-border"
|
| 1283 |
+
/>
|
| 1284 |
+
{/each}
|
| 1285 |
<polygon
|
| 1286 |
+
points={selectedModelRadarPolygon}
|
| 1287 |
+
fill="rgba(13, 206, 151, 0.22)"
|
| 1288 |
+
stroke="#0dce97"
|
| 1289 |
+
stroke-width="2"
|
|
|
|
|
|
|
| 1290 |
/>
|
| 1291 |
+
{#each selectedModelRadarPoints as point (point.name)}
|
| 1292 |
+
<circle cx={point.x} cy={point.y} r="3" fill="#0dce97" />
|
| 1293 |
+
{/each}
|
| 1294 |
+
</svg>
|
| 1295 |
+
<RadarLabelsOverlay
|
| 1296 |
+
labels={selectedModelRadarPoints.map((point, index) => {
|
| 1297 |
+
const [labelX, labelY] = radarPoint(
|
| 1298 |
+
index,
|
| 1299 |
+
selectedModelRadarPoints.length,
|
| 1300 |
+
modelSheetRadarLabelRadius
|
| 1301 |
+
);
|
| 1302 |
+
const adjustedLabelX =
|
| 1303 |
+
160 + (labelX - 160) * modelSheetRadarHorizontalLabelScale;
|
| 1304 |
+
return {
|
| 1305 |
+
key: point.name,
|
| 1306 |
+
text: point.name,
|
| 1307 |
+
x: adjustedLabelX,
|
| 1308 |
+
y: labelY + 3,
|
| 1309 |
+
anchor:
|
| 1310 |
+
adjustedLabelX < 155 ? 'end' : adjustedLabelX > 165 ? 'start' : 'middle'
|
| 1311 |
+
};
|
| 1312 |
+
})}
|
| 1313 |
+
viewBoxWidth={modelSheetRadarViewBoxSize}
|
| 1314 |
+
viewBoxHeight={modelSheetRadarViewBoxSize}
|
| 1315 |
+
labelClass="text-[9px]"
|
| 1316 |
+
position="label"
|
| 1317 |
/>
|
| 1318 |
+
</div>
|
|
|
|
|
|
|
|
|
|
| 1319 |
</div>
|
| 1320 |
<div
|
| 1321 |
class="flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-xs text-muted-foreground"
|
|
|
|
| 1423 |
<div class="space-y-3">
|
| 1424 |
{#each selectedModelGroups as group (group.name)}
|
| 1425 |
<div class="grid items-center gap-3 sm:grid-cols-[minmax(9rem,13rem)_1fr_auto]">
|
| 1426 |
+
<OverflowTooltip
|
| 1427 |
+
text={group.name}
|
| 1428 |
+
class="block truncate font-mono text-xs font-semibold"
|
| 1429 |
+
triggerClass="block min-w-0"
|
| 1430 |
+
contentClass="max-w-sm break-all"
|
| 1431 |
+
/>
|
| 1432 |
<div class="h-2 rounded-full bg-muted">
|
| 1433 |
<div
|
| 1434 |
class="h-full rounded-full bg-[#0dce97]"
|
|
@@ -1,17 +1,25 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import {
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import BucketDataFallback from '$lib/components/BucketDataFallback.svelte';
|
| 5 |
import BucketDataWarning from '$lib/components/BucketDataWarning.svelte';
|
|
|
|
| 6 |
import PageHeadingSection from '$lib/components/PageHeadingSection.svelte';
|
| 7 |
import Select from '$lib/components/Select.svelte';
|
| 8 |
import Table, { type TableColumn } from '$lib/components/Table.svelte';
|
|
|
|
|
|
|
| 9 |
import { Badge } from '$lib/components/ui/badge';
|
| 10 |
import { Button } from '$lib/components/ui/button';
|
| 11 |
import { Checkbox } from '$lib/components/ui/checkbox';
|
| 12 |
import {
|
| 13 |
toolsActiveGroups,
|
| 14 |
-
toolsBarLabelLines as barLabelLines,
|
| 15 |
toolsBarY as barY,
|
| 16 |
toolsCsvDataUri,
|
| 17 |
toolsDataGroups,
|
|
@@ -31,12 +39,10 @@
|
|
| 31 |
import { isNumber } from '$lib/helpers/leaderboard';
|
| 32 |
import type { GuardModel } from '$lib/types/leaderboard';
|
| 33 |
import type {
|
| 34 |
-
ToolsChartTooltip as ChartTooltip,
|
| 35 |
ToolsLanguageFilter as LangFilter,
|
| 36 |
ToolsLatencySortKey as LatencySortKey,
|
| 37 |
ToolsRadarScale as RadarScale,
|
| 38 |
ToolsSortDirection as SortDirection,
|
| 39 |
-
ToolsTooltipRow as TooltipRow,
|
| 40 |
ToolsVizMetric as VizMetric
|
| 41 |
} from '$lib/types/tools';
|
| 42 |
import { m } from '$lib/paraglide/messages';
|
|
@@ -59,7 +65,6 @@
|
|
| 59 |
type ParetoRow = VisualizationModelRow & {
|
| 60 |
p95: number;
|
| 61 |
integral: number;
|
| 62 |
-
fpr: number | null;
|
| 63 |
};
|
| 64 |
type PerformanceRow = VisualizationModelRow & {
|
| 65 |
rank: number;
|
|
@@ -76,6 +81,9 @@
|
|
| 76 |
fnrReal: number | null;
|
| 77 |
fnrRobust: number | null;
|
| 78 |
deltaFnr: number | null;
|
|
|
|
|
|
|
|
|
|
| 79 |
category: string;
|
| 80 |
};
|
| 81 |
type HeatmapRow = ToolsDataset['visualizations']['heatmap']['rows'][number];
|
|
@@ -84,6 +92,14 @@
|
|
| 84 |
max: number | null;
|
| 85 |
};
|
| 86 |
const selectContentClass = 'max-h-72';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
let { data }: PageProps = $props();
|
| 88 |
const toolsState = $derived(data.toolsState);
|
| 89 |
const tools = $derived(toolsState.data);
|
|
@@ -94,7 +110,7 @@
|
|
| 94 |
const initialTools = (() => data.toolsState.data)();
|
| 95 |
const initialGroups = toolsDataGroups(initialTools.models, initialTools.allGroups);
|
| 96 |
|
| 97 |
-
let selectedModelKeys = $state(initialTools.models.map((model) => modelKey(model)));
|
| 98 |
let selectedGroups = $state<string[]>(initialGroups);
|
| 99 |
let language = $state<LangFilter>('all');
|
| 100 |
let metric = $state<VizMetric>('fnr');
|
|
@@ -102,12 +118,11 @@
|
|
| 102 |
let pinnedModelKeys = $state<string[]>([]);
|
| 103 |
let latencySortKey = $state<LatencySortKey>('p95');
|
| 104 |
let latencySortDirection = $state<SortDirection>('asc');
|
| 105 |
-
let chartTooltip = $state<ChartTooltip | null>(null);
|
| 106 |
-
let chartTooltipElement: HTMLDivElement | null = $state(null);
|
| 107 |
let hoveredRadarModelKey = $state<string | null>(null);
|
| 108 |
let hoveredScatterModelKey = $state<string | null>(null);
|
| 109 |
let hoveredBarKey = $state<string | null>(null);
|
| 110 |
let hoveredParetoModelKey = $state<string | null>(null);
|
|
|
|
| 111 |
|
| 112 |
const languageOptions = [
|
| 113 |
{ value: 'all', label: m.tools_language_all() },
|
|
@@ -123,11 +138,36 @@
|
|
| 123 |
{ value: 'f1', label: 'F1' }
|
| 124 |
];
|
| 125 |
const latencyColumns: LatencyColumn[] = [
|
| 126 |
-
{
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
];
|
| 132 |
|
| 133 |
const currentLanguageLabel = $derived(
|
|
@@ -158,6 +198,7 @@
|
|
| 158 |
const heatColumns = $derived(
|
| 159 |
`minmax(8rem, 10rem) repeat(${heatmapModels.length}, minmax(4rem, 1fr))`
|
| 160 |
);
|
|
|
|
| 161 |
const scatterRows = $derived(
|
| 162 |
scatterRowsFor(tools.visualizations.scatter.points, modelByKey, selectedModelKeySet)
|
| 163 |
);
|
|
@@ -181,9 +222,11 @@
|
|
| 181 |
const robustRows = $derived(
|
| 182 |
robustnessRowsFor(tools.visualizations.robustness.rows, modelByKey, selectedModelKeySet)
|
| 183 |
);
|
| 184 |
-
const scatterXMax =
|
| 185 |
-
const scatterYMax =
|
| 186 |
-
const barSvgWidth = $derived(
|
|
|
|
|
|
|
| 187 |
const paretoXMax = $derived(Math.max(1, ...paretoRows.map((row) => row.p95)) * 1.08);
|
| 188 |
const paretoY0 = $derived(paretoYStart(paretoRows));
|
| 189 |
const paretoXTicks = $derived(niceTicks(0, paretoXMax, 6));
|
|
@@ -214,6 +257,10 @@
|
|
| 214 |
return `${Math.round(clamp01(value ?? 0) * 100)}%`;
|
| 215 |
}
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
function scatterRowsFor(
|
| 218 |
points: ToolsDataset['visualizations']['scatter']['points'],
|
| 219 |
modelsByKey: ReadonlyMap<string, GuardModel>,
|
|
@@ -250,9 +297,7 @@
|
|
| 250 |
) {
|
| 251 |
return [];
|
| 252 |
}
|
| 253 |
-
return [
|
| 254 |
-
{ modelKey: key, model, p95: row.latencyMs.p95, integral: row.integral, fpr: row.fpr }
|
| 255 |
-
];
|
| 256 |
});
|
| 257 |
}
|
| 258 |
|
|
@@ -312,6 +357,9 @@
|
|
| 312 |
fnrReal: row.fnrReal,
|
| 313 |
fnrRobust: row.fnrRobust,
|
| 314 |
deltaFnr: row.deltaFnr,
|
|
|
|
|
|
|
|
|
|
| 315 |
category: localized(row.categoryLabel, '')
|
| 316 |
}
|
| 317 |
];
|
|
@@ -340,17 +388,16 @@
|
|
| 340 |
}
|
| 341 |
|
| 342 |
function groupedBarsMean(
|
| 343 |
-
|
| 344 |
key: VizMetric,
|
| 345 |
modelsByVisualizationKey: ReadonlyMap<
|
| 346 |
string,
|
| 347 |
ToolsDataset['visualizations']['groupedBars']['models'][number]
|
| 348 |
>,
|
| 349 |
-
|
| 350 |
) {
|
| 351 |
-
const
|
| 352 |
-
|
| 353 |
-
.filter(isNumber);
|
| 354 |
|
| 355 |
if (!values.length) return null;
|
| 356 |
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
@@ -375,7 +422,8 @@
|
|
| 375 |
}
|
| 376 |
|
| 377 |
function paretoLabelText(row: ParetoRow) {
|
| 378 |
-
|
|
|
|
| 379 |
}
|
| 380 |
|
| 381 |
function paretoYStart(rows: readonly ParetoRow[]) {
|
|
@@ -430,57 +478,6 @@
|
|
| 430 |
action();
|
| 431 |
}
|
| 432 |
|
| 433 |
-
function tooltipPosition(event: Pick<PointerEvent, 'clientX' | 'clientY'>) {
|
| 434 |
-
const margin = 12;
|
| 435 |
-
const offset = 16;
|
| 436 |
-
const tooltipRect = chartTooltipElement?.getBoundingClientRect();
|
| 437 |
-
const tooltipWidth = tooltipRect?.width ?? 384;
|
| 438 |
-
const tooltipHeight = tooltipRect?.height ?? 180;
|
| 439 |
-
let x = event.clientX + offset;
|
| 440 |
-
let y = event.clientY + offset;
|
| 441 |
-
|
| 442 |
-
if (typeof window === 'undefined') return { x, y };
|
| 443 |
-
|
| 444 |
-
if (x + tooltipWidth + margin > window.innerWidth) {
|
| 445 |
-
x = event.clientX - tooltipWidth - offset;
|
| 446 |
-
}
|
| 447 |
-
if (y + tooltipHeight + margin > window.innerHeight) {
|
| 448 |
-
y = event.clientY - tooltipHeight - offset;
|
| 449 |
-
}
|
| 450 |
-
|
| 451 |
-
return {
|
| 452 |
-
x: Math.min(Math.max(margin, x), Math.max(margin, window.innerWidth - tooltipWidth - margin)),
|
| 453 |
-
y: Math.min(
|
| 454 |
-
Math.max(margin, y),
|
| 455 |
-
Math.max(margin, window.innerHeight - tooltipHeight - margin)
|
| 456 |
-
)
|
| 457 |
-
};
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
function updateTooltip(event: PointerEvent, title: string, rows: TooltipRow[]) {
|
| 461 |
-
const position = tooltipPosition(event);
|
| 462 |
-
chartTooltip = {
|
| 463 |
-
x: position.x,
|
| 464 |
-
y: position.y,
|
| 465 |
-
title,
|
| 466 |
-
rows
|
| 467 |
-
};
|
| 468 |
-
|
| 469 |
-
tick().then(() => {
|
| 470 |
-
if (!chartTooltip) return;
|
| 471 |
-
chartTooltip = { ...chartTooltip, ...tooltipPosition(event) };
|
| 472 |
-
});
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
function moveTooltip(event: PointerEvent) {
|
| 476 |
-
if (!chartTooltip) return;
|
| 477 |
-
chartTooltip = { ...chartTooltip, ...tooltipPosition(event) };
|
| 478 |
-
}
|
| 479 |
-
|
| 480 |
-
function hideTooltip() {
|
| 481 |
-
chartTooltip = null;
|
| 482 |
-
}
|
| 483 |
-
|
| 484 |
function toggleLatencySort(key: LatencySortKey) {
|
| 485 |
if (latencySortKey === key) {
|
| 486 |
latencySortDirection = latencySortDirection === 'asc' ? 'desc' : 'asc';
|
|
@@ -495,12 +492,20 @@
|
|
| 495 |
return typeof column.label === 'function' ? column.label() : column.label;
|
| 496 |
}
|
| 497 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
function heatTextColor(value: number | null | undefined) {
|
| 499 |
if (!isNumber(value)) return 'text-muted-foreground';
|
| 500 |
return 'text-black';
|
| 501 |
}
|
| 502 |
|
| 503 |
-
function deltaBadgeClass(value: number | null, kind: 'score' | 'fnr') {
|
| 504 |
if (!isNumber(value) || value === 0) {
|
| 505 |
return 'bg-muted/40 text-muted-foreground';
|
| 506 |
}
|
|
@@ -526,995 +531,1045 @@
|
|
| 526 |
<BucketDataFallback />
|
| 527 |
</div>
|
| 528 |
{:else}
|
| 529 |
-
<
|
| 530 |
-
{#
|
| 531 |
-
<
|
| 532 |
-
|
| 533 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
</div>
|
| 560 |
</div>
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
<div
|
| 565 |
-
class="flex
|
| 566 |
-
role="
|
| 567 |
-
|
| 568 |
-
aria-checked={selectedModelKeys.includes(key)}
|
| 569 |
-
onclick={() => (selectedModelKeys = toggleListValue(selectedModelKeys, key))}
|
| 570 |
-
onkeydown={(event) =>
|
| 571 |
-
handleCheckboxKeydown(
|
| 572 |
-
event,
|
| 573 |
-
() => (selectedModelKeys = toggleListValue(selectedModelKeys, key))
|
| 574 |
-
)}
|
| 575 |
>
|
| 576 |
-
<Checkbox
|
| 577 |
-
checked={selectedModelKeys.includes(key)}
|
| 578 |
-
aria-label={model.short}
|
| 579 |
-
tabindex={-1}
|
| 580 |
-
/>
|
| 581 |
-
<span class="size-3 rounded-sm" style={`background:${colorOf(model)}`}></span>
|
| 582 |
-
<span class="min-w-0 flex-1 truncate">
|
| 583 |
-
{model.short} <code class="text-muted-foreground">{model.params}</code>
|
| 584 |
-
</span>
|
| 585 |
<Button
|
| 586 |
-
variant=
|
| 587 |
size="sm"
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
pinnedModelKeys.includes(key) &&
|
| 591 |
-
'border-[#0dce97]/60 bg-[#0dce97]/10 text-[#0dce97] hover:bg-[#0dce97]/15 hover:text-[#0dce97]'
|
| 592 |
-
)}
|
| 593 |
-
aria-pressed={pinnedModelKeys.includes(key)}
|
| 594 |
-
onclick={(event) => {
|
| 595 |
-
event.preventDefault();
|
| 596 |
-
event.stopPropagation();
|
| 597 |
-
togglePin(key);
|
| 598 |
-
}}
|
| 599 |
>
|
| 600 |
-
|
| 601 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
</Button>
|
| 603 |
</div>
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
<div class="min-w-0">
|
| 609 |
-
<div class="mb-2 flex items-center justify-between gap-3">
|
| 610 |
-
<h2 class="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
| 611 |
-
{m.tools_groups_title()}
|
| 612 |
-
</h2>
|
| 613 |
-
<div class="flex gap-2 text-xs">
|
| 614 |
-
<Button
|
| 615 |
-
variant="outline"
|
| 616 |
-
size="sm"
|
| 617 |
-
class="h-8 rounded-full px-4"
|
| 618 |
-
onclick={() => (selectedGroups = [...dataGroups])}>{m.details_select_all()}</Button
|
| 619 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
<Button
|
| 621 |
-
|
|
|
|
| 622 |
size="sm"
|
| 623 |
-
class="
|
| 624 |
-
onclick={() => (selectedGroups = [])}>{m.details_reset()}</Button
|
| 625 |
>
|
|
|
|
|
|
|
|
|
|
| 626 |
</div>
|
| 627 |
</div>
|
| 628 |
-
|
| 629 |
-
{#each dataGroups as group (group)}
|
| 630 |
-
<div
|
| 631 |
-
class="flex min-w-0 items-center gap-3 rounded-md px-2 py-1.5 text-sm hover:bg-muted/50"
|
| 632 |
-
role="checkbox"
|
| 633 |
-
tabindex="0"
|
| 634 |
-
aria-checked={selectedGroups.includes(group)}
|
| 635 |
-
onclick={() => (selectedGroups = toggleListValue(selectedGroups, group))}
|
| 636 |
-
onkeydown={(event) =>
|
| 637 |
-
handleCheckboxKeydown(
|
| 638 |
-
event,
|
| 639 |
-
() => (selectedGroups = toggleListValue(selectedGroups, group))
|
| 640 |
-
)}
|
| 641 |
-
>
|
| 642 |
-
<Checkbox
|
| 643 |
-
checked={selectedGroups.includes(group)}
|
| 644 |
-
aria-label={groupLabel(group)}
|
| 645 |
-
tabindex={-1}
|
| 646 |
-
/>
|
| 647 |
-
<span class="min-w-0 truncate">
|
| 648 |
-
{groupLabel(group)}
|
| 649 |
-
<span class="text-muted-foreground"
|
| 650 |
-
>{benchmarkMeta[group]?.languages.join(' · ')}</span
|
| 651 |
-
>
|
| 652 |
-
</span>
|
| 653 |
-
</div>
|
| 654 |
-
{/each}
|
| 655 |
-
</div>
|
| 656 |
-
</div>
|
| 657 |
|
| 658 |
-
<div
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
options={languageOptions}
|
| 664 |
-
label={m.tools_language_label()}
|
| 665 |
-
triggerLabel={currentLanguageLabel}
|
| 666 |
-
contentClass={selectContentClass}
|
| 667 |
-
/>
|
| 668 |
-
<Select
|
| 669 |
-
id="tools-metric"
|
| 670 |
-
type="single"
|
| 671 |
-
bind:value={metric}
|
| 672 |
-
options={metricOptions}
|
| 673 |
-
label={m.tools_metric_label()}
|
| 674 |
-
triggerLabel={currentMetricLabel}
|
| 675 |
-
contentClass={selectContentClass}
|
| 676 |
-
/>
|
| 677 |
-
</div>
|
| 678 |
-
</div>
|
| 679 |
-
|
| 680 |
-
<div class="mt-4 flex flex-wrap items-end gap-3">
|
| 681 |
-
<div class="min-w-0 space-y-2">
|
| 682 |
-
<span class="block text-xs font-semibold tracking-wider text-muted-foreground uppercase"
|
| 683 |
-
>{m.tools_radar_scale()}</span
|
| 684 |
-
>
|
| 685 |
-
<div
|
| 686 |
-
class="inline-flex max-w-full flex-wrap gap-1 rounded-4xl border bg-input/50 p-1"
|
| 687 |
-
role="group"
|
| 688 |
-
aria-label={m.tools_radar_scale()}
|
| 689 |
-
>
|
| 690 |
-
<Button
|
| 691 |
-
variant={radarScale === 'score' ? 'default' : 'ghost'}
|
| 692 |
-
size="sm"
|
| 693 |
-
aria-pressed={radarScale === 'score'}
|
| 694 |
-
onclick={() => (radarScale = 'score')}
|
| 695 |
-
>
|
| 696 |
-
{m.tools_radar_group_score()}
|
| 697 |
-
</Button>
|
| 698 |
-
<Button
|
| 699 |
-
variant={radarScale === 'f1' ? 'default' : 'ghost'}
|
| 700 |
-
size="sm"
|
| 701 |
-
aria-pressed={radarScale === 'f1'}
|
| 702 |
-
onclick={() => (radarScale = 'f1')}
|
| 703 |
-
>
|
| 704 |
-
F1
|
| 705 |
-
</Button>
|
| 706 |
-
</div>
|
| 707 |
-
</div>
|
| 708 |
-
<div class="ml-auto flex flex-wrap items-center justify-end gap-3">
|
| 709 |
-
<span
|
| 710 |
-
class="rounded-full border border-[#0dce97]/40 bg-[#0dce97]/10 px-4 py-2 font-mono text-xs font-semibold text-[#0dce97]"
|
| 711 |
-
>
|
| 712 |
-
{m.tools_selected_summary({
|
| 713 |
-
models: selectedModels.length,
|
| 714 |
-
groups: activeGroups.length
|
| 715 |
-
})}
|
| 716 |
-
</span>
|
| 717 |
-
<Button
|
| 718 |
-
href={csvHref}
|
| 719 |
-
download="hivetrace_tools_selection.csv"
|
| 720 |
-
size="sm"
|
| 721 |
-
class="bg-[#0dce97] text-emerald-950 hover:bg-[#0dce97]/90"
|
| 722 |
>
|
| 723 |
-
|
| 724 |
-
{
|
| 725 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
</div>
|
| 727 |
-
</div>
|
| 728 |
-
</section>
|
| 729 |
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
</div>
|
| 763 |
-
{#if selectedModels.length && activeGroups.length >= 3}
|
| 764 |
-
<div class="overflow-hidden">
|
| 765 |
<svg
|
| 766 |
viewBox="0 0 520 420"
|
| 767 |
-
class="h-auto w-full"
|
| 768 |
role="img"
|
| 769 |
-
aria-label={m.
|
| 770 |
>
|
| 771 |
-
{#each
|
| 772 |
-
<
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
})
|
| 778 |
-
.join(' ')}
|
| 779 |
-
fill="none"
|
| 780 |
stroke="currentColor"
|
| 781 |
stroke-width="1"
|
| 782 |
stroke-dasharray="3 4"
|
| 783 |
class="text-border"
|
| 784 |
/>
|
| 785 |
-
<text
|
| 786 |
-
x="264"
|
| 787 |
-
y={214 - 150 * scale}
|
| 788 |
-
class="fill-muted-foreground font-mono text-[10px]"
|
| 789 |
-
>
|
| 790 |
-
{scale.toFixed(2)}
|
| 791 |
-
</text>
|
| 792 |
-
{/each}
|
| 793 |
-
{#each activeGroups as group, index (group)}
|
| 794 |
-
{@const axis = radarPoint(index, activeGroups.length, 150)}
|
| 795 |
-
{@const label = radarPoint(index, activeGroups.length, 178)}
|
| 796 |
<line
|
| 797 |
-
x1="
|
| 798 |
-
y1=
|
| 799 |
-
x2=
|
| 800 |
-
y2={
|
| 801 |
stroke="currentColor"
|
| 802 |
stroke-width="1"
|
|
|
|
| 803 |
class="text-border"
|
| 804 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 805 |
<text
|
| 806 |
-
x=
|
| 807 |
-
y={
|
| 808 |
-
text-anchor=
|
| 809 |
-
|
| 810 |
-
: label.x > 260
|
| 811 |
-
? 'start'
|
| 812 |
-
: 'end'}
|
| 813 |
-
class="fill-muted-foreground font-mono text-[10px]"
|
| 814 |
>
|
| 815 |
-
{groupLabel(group).length > 14
|
| 816 |
-
? `${groupLabel(group).slice(0, 13)}…`
|
| 817 |
-
: groupLabel(group)}
|
| 818 |
-
</text>
|
| 819 |
{/each}
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
/>
|
| 837 |
-
{#
|
| 838 |
-
{@const value = radarVisualizationValue(key, group, radarScale) ?? 0}
|
| 839 |
-
{@const point = radarPoint(index, activeGroups.length, 150 * clamp01(value))}
|
| 840 |
<circle
|
| 841 |
cx={point.x}
|
| 842 |
cy={point.y}
|
| 843 |
-
r=
|
| 844 |
-
fill=
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
aria-label={`${model.short} ${groupLabel(group)} ${radarScale.toUpperCase()} ${fmt(value, 3)}`}
|
| 848 |
-
onpointerenter={(event) => {
|
| 849 |
-
hoveredRadarModelKey = key;
|
| 850 |
-
updateTooltip(event, model.short, [
|
| 851 |
-
{ label: groupLabel(group), value: radarScale.toUpperCase() },
|
| 852 |
-
{ label: 'value', value: fmt(value, 3) }
|
| 853 |
-
]);
|
| 854 |
-
}}
|
| 855 |
-
onpointermove={moveTooltip}
|
| 856 |
-
onpointerleave={() => {
|
| 857 |
-
hoveredRadarModelKey = null;
|
| 858 |
-
hideTooltip();
|
| 859 |
-
}}
|
| 860 |
/>
|
| 861 |
-
{/
|
| 862 |
{/each}
|
| 863 |
</svg>
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
{#each selectedModels as model (modelKey(model))}
|
| 867 |
-
{@const key = modelKey(model)}
|
| 868 |
-
{@const isPinned = pinnedModelKeys.includes(key)}
|
| 869 |
-
<span
|
| 870 |
-
class={cn(
|
| 871 |
-
'inline-flex min-w-0 max-w-full items-center gap-2 rounded-full border border-transparent px-2 py-1 transition-colors',
|
| 872 |
-
isPinned &&
|
| 873 |
-
'border-[#0dce97]/35 bg-[#0dce97]/10 text-foreground shadow-sm shadow-[#0dce97]/5'
|
| 874 |
-
)}
|
| 875 |
-
>
|
| 876 |
-
<span class="size-2 rounded-full" style={`background:${colorOf(model)}`}></span>
|
| 877 |
-
{#if isPinned}
|
| 878 |
-
<PinIcon class="size-3 text-[#0dce97]" />
|
| 879 |
-
{/if}
|
| 880 |
-
<span class="min-w-0 truncate">{model.short}</span>
|
| 881 |
-
<button
|
| 882 |
-
class="font-semibold text-[#0dce97]"
|
| 883 |
-
type="button"
|
| 884 |
-
onclick={() => togglePin(key)}
|
| 885 |
-
>
|
| 886 |
-
{isPinned ? m.tools_unpin() : m.tools_pin()}
|
| 887 |
-
</button>
|
| 888 |
-
</span>
|
| 889 |
-
{/each}
|
| 890 |
-
</div>
|
| 891 |
-
{:else}
|
| 892 |
-
<p class="rounded-lg border bg-muted/30 p-6 text-center text-sm text-muted-foreground">
|
| 893 |
-
{m.tools_radar_empty()}
|
| 894 |
-
</p>
|
| 895 |
-
{/if}
|
| 896 |
-
</section>
|
| 897 |
-
|
| 898 |
-
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm">
|
| 899 |
-
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 900 |
-
<h2 class="font-mono text-sm font-semibold">FPR × FNR</h2>
|
| 901 |
-
<span class="text-xs text-muted-foreground">{m.tools_scatter_hint()}</span>
|
| 902 |
-
</div>
|
| 903 |
-
<svg
|
| 904 |
-
viewBox="0 0 520 420"
|
| 905 |
-
class="h-auto w-full"
|
| 906 |
-
role="img"
|
| 907 |
-
aria-label={m.tools_scatter_aria()}
|
| 908 |
-
>
|
| 909 |
-
{#each [0.25, 0.5, 0.75, 1] as ratio (ratio)}
|
| 910 |
-
{@const xt = scatterXMax * ratio}
|
| 911 |
-
{@const yt = scatterYMax * ratio}
|
| 912 |
-
<line
|
| 913 |
-
x1={scatterX(xt, scatterXMax)}
|
| 914 |
-
y1="18"
|
| 915 |
-
x2={scatterX(xt, scatterXMax)}
|
| 916 |
-
y2="368"
|
| 917 |
-
stroke="currentColor"
|
| 918 |
-
stroke-width="1"
|
| 919 |
-
stroke-dasharray="3 4"
|
| 920 |
-
class="text-border"
|
| 921 |
-
/>
|
| 922 |
-
<line
|
| 923 |
-
x1="52"
|
| 924 |
-
y1={scatterY(yt, scatterYMax)}
|
| 925 |
-
x2="502"
|
| 926 |
-
y2={scatterY(yt, scatterYMax)}
|
| 927 |
-
stroke="currentColor"
|
| 928 |
-
stroke-width="1"
|
| 929 |
-
stroke-dasharray="3 4"
|
| 930 |
-
class="text-border"
|
| 931 |
-
/>
|
| 932 |
-
{/each}
|
| 933 |
-
{#each [0, 0.25, 0.5, 0.75, 1] as ratio (ratio)}
|
| 934 |
-
{@const xt = scatterXMax * ratio}
|
| 935 |
-
{@const yt = scatterYMax * ratio}
|
| 936 |
-
<text
|
| 937 |
-
x={scatterX(xt, scatterXMax)}
|
| 938 |
-
y="386"
|
| 939 |
-
text-anchor="middle"
|
| 940 |
-
class="fill-muted-foreground font-mono text-[10px]">{xt.toFixed(2)}</text
|
| 941 |
-
>
|
| 942 |
-
<text
|
| 943 |
-
x="44"
|
| 944 |
-
y={scatterY(yt, scatterYMax) + 3}
|
| 945 |
-
text-anchor="end"
|
| 946 |
-
class="fill-muted-foreground font-mono text-[10px]">{yt.toFixed(2)}</text
|
| 947 |
-
>
|
| 948 |
-
{/each}
|
| 949 |
-
<line x1="52" y1="18" x2="52" y2="368" stroke="currentColor" class="text-border" />
|
| 950 |
-
<line x1="52" y1="368" x2="502" y2="368" stroke="currentColor" class="text-border" />
|
| 951 |
-
<text
|
| 952 |
-
x="277"
|
| 953 |
-
y="408"
|
| 954 |
-
text-anchor="middle"
|
| 955 |
-
class="fill-muted-foreground font-mono text-[10px]">FPR →</text
|
| 956 |
-
>
|
| 957 |
-
<text
|
| 958 |
-
transform="translate(16,193) rotate(-90)"
|
| 959 |
-
text-anchor="middle"
|
| 960 |
-
class="fill-muted-foreground font-mono text-[10px]">FNR →</text
|
| 961 |
-
>
|
| 962 |
-
<text
|
| 963 |
-
x={scatterX(0, scatterXMax)}
|
| 964 |
-
y={scatterY(0, scatterYMax)}
|
| 965 |
-
text-anchor="middle"
|
| 966 |
-
dominant-baseline="central"
|
| 967 |
-
class="fill-[#0dce97] text-sm">★</text
|
| 968 |
-
>
|
| 969 |
-
{#each scatterRows as row (row.modelKey)}
|
| 970 |
-
{@const point = {
|
| 971 |
-
x: scatterX(row.fpr, scatterXMax),
|
| 972 |
-
y: scatterY(row.fnr, scatterYMax)
|
| 973 |
-
}}
|
| 974 |
-
{@const isPinned = pinnedModelKeys.includes(row.modelKey)}
|
| 975 |
-
{@const isHovered = hoveredScatterModelKey === row.modelKey}
|
| 976 |
-
<circle
|
| 977 |
-
cx={point.x}
|
| 978 |
-
cy={point.y}
|
| 979 |
-
r={isHovered ? 9 : isPinned ? 8 : 6}
|
| 980 |
-
fill={colorOf(row.model)}
|
| 981 |
-
fill-opacity="0.85"
|
| 982 |
-
stroke="currentColor"
|
| 983 |
-
stroke-width={isPinned ? 2 : 1}
|
| 984 |
-
class="text-background transition-[r]"
|
| 985 |
-
role="img"
|
| 986 |
-
aria-label={`${row.model.short} FPR ${fmt(row.fpr, 3)} FNR ${fmt(row.fnr, 3)}`}
|
| 987 |
-
onpointerenter={(event) => {
|
| 988 |
-
hoveredScatterModelKey = row.modelKey;
|
| 989 |
-
updateTooltip(event, row.model.short, [
|
| 990 |
-
{ label: 'FPR', value: fmt(row.fpr, 3) },
|
| 991 |
-
{ label: 'FNR', value: fmt(row.fnr, 3) }
|
| 992 |
-
]);
|
| 993 |
-
}}
|
| 994 |
-
onpointermove={moveTooltip}
|
| 995 |
-
onpointerleave={() => {
|
| 996 |
-
hoveredScatterModelKey = null;
|
| 997 |
-
hideTooltip();
|
| 998 |
-
}}
|
| 999 |
-
/>
|
| 1000 |
-
{#if isPinned}
|
| 1001 |
-
<circle
|
| 1002 |
-
cx={point.x}
|
| 1003 |
-
cy={point.y}
|
| 1004 |
-
r="11"
|
| 1005 |
-
fill="none"
|
| 1006 |
-
stroke={colorOf(row.model)}
|
| 1007 |
-
stroke-width="1.5"
|
| 1008 |
-
/>
|
| 1009 |
-
{/if}
|
| 1010 |
-
{/each}
|
| 1011 |
-
</svg>
|
| 1012 |
-
<div class="mt-3 flex flex-wrap gap-4 text-xs text-muted-foreground">
|
| 1013 |
-
<span class="text-[#0dce97]">{m.tools_scatter_ideal()}</span>
|
| 1014 |
-
</div>
|
| 1015 |
-
</section>
|
| 1016 |
-
|
| 1017 |
-
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1018 |
-
<div class="mb-3 flex flex-wrap items-center gap-3">
|
| 1019 |
-
<h2 class="font-mono text-sm font-semibold">
|
| 1020 |
-
Heatmap <span class="font-mono uppercase">{heatmapMetric}</span>
|
| 1021 |
-
</h2>
|
| 1022 |
-
<span class="ml-auto inline-flex items-center gap-2 text-xs text-muted-foreground">
|
| 1023 |
-
{m.tools_heatmap_scale_min({ value: heatmapScaleLabel(heatmapColorScale.min) })}
|
| 1024 |
-
<span class="h-2 w-28 rounded-full bg-[linear-gradient(90deg,#0dce97,#ffca28,#ff4d4f)]"
|
| 1025 |
-
></span>
|
| 1026 |
-
{m.tools_heatmap_scale_max({ value: heatmapScaleLabel(heatmapColorScale.max) })}
|
| 1027 |
-
</span>
|
| 1028 |
-
</div>
|
| 1029 |
-
<div class="max-w-full overflow-auto">
|
| 1030 |
-
<div class="grid min-w-[54rem] gap-1" style:grid-template-columns={heatColumns}>
|
| 1031 |
-
<div
|
| 1032 |
-
class="px-2 py-2 font-mono text-[10px] text-muted-foreground grid place-items-center"
|
| 1033 |
-
>
|
| 1034 |
-
{m.tools_heatmap_header({ metric: heatmapMetric.toUpperCase() })}
|
| 1035 |
</div>
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1045 |
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
</div>
|
| 1047 |
-
{
|
| 1048 |
-
{#each heatmapRows as row (row.id)}
|
| 1049 |
<div
|
| 1050 |
-
class="
|
| 1051 |
>
|
| 1052 |
-
|
| 1053 |
</div>
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1059 |
role="img"
|
| 1060 |
-
|
| 1061 |
-
'w-full rounded-md px-2 py-2 text-center font-mono text-sm font-semibold',
|
| 1062 |
-
hasValue && 'transition duration-150 hover:brightness-110 hover:saturate-125',
|
| 1063 |
-
!isNumber(value) && 'bg-muted/70 text-muted-foreground',
|
| 1064 |
-
isNumber(value) && heatTextColor(value)
|
| 1065 |
-
)}
|
| 1066 |
-
style={isNumber(value)
|
| 1067 |
-
? `background:${heatColor(
|
| 1068 |
-
value,
|
| 1069 |
-
heatmapColorScale.min ?? value,
|
| 1070 |
-
heatmapColorScale.max ?? value,
|
| 1071 |
-
'sqrt'
|
| 1072 |
-
)}`
|
| 1073 |
-
: ''}
|
| 1074 |
-
aria-label={`${model.short} ${heatmapRowLabel(row)} ${heatmapMetric.toUpperCase()} ${fmt(value, 3)}`}
|
| 1075 |
-
onpointerenter={(event) =>
|
| 1076 |
-
updateTooltip(event, model.short, [
|
| 1077 |
-
{ label: groupLabel(row.groupKey), value: heatmapRowLabel(row) },
|
| 1078 |
-
{
|
| 1079 |
-
label: heatmapMetric.toUpperCase(),
|
| 1080 |
-
value: fmt(value, 3)
|
| 1081 |
-
}
|
| 1082 |
-
])}
|
| 1083 |
-
onpointermove={moveTooltip}
|
| 1084 |
-
onpointerleave={hideTooltip}
|
| 1085 |
>
|
| 1086 |
-
{
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1093 |
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
>
|
| 1106 |
-
{#each [0, 0.25, 0.5, 0.75, 1] as tick (tick)}
|
| 1107 |
-
<line
|
| 1108 |
-
x1="44"
|
| 1109 |
-
y1={barY(tick)}
|
| 1110 |
-
x2={barSvgWidth - 12}
|
| 1111 |
-
y2={barY(tick)}
|
| 1112 |
-
stroke="currentColor"
|
| 1113 |
-
stroke-width="1"
|
| 1114 |
-
stroke-dasharray="3 4"
|
| 1115 |
-
class="text-border"
|
| 1116 |
-
/>
|
| 1117 |
-
<text
|
| 1118 |
-
x="38"
|
| 1119 |
-
y={barY(tick) + 3}
|
| 1120 |
-
text-anchor="end"
|
| 1121 |
-
class="fill-muted-foreground font-mono text-[13px]">{tick.toFixed(2)}</text
|
| 1122 |
>
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
{
|
| 1154 |
-
label: series.key.toUpperCase(),
|
| 1155 |
-
value: fmt(value, 3)
|
| 1156 |
-
},
|
| 1157 |
-
{
|
| 1158 |
-
label: m.tools_mean(),
|
| 1159 |
-
value: m.tools_selected_summary({
|
| 1160 |
-
models: selectedModels.length,
|
| 1161 |
-
groups: activeGroups.length
|
| 1162 |
-
})
|
| 1163 |
-
}
|
| 1164 |
-
]);
|
| 1165 |
-
}}
|
| 1166 |
-
onpointermove={moveTooltip}
|
| 1167 |
-
onpointerleave={() => {
|
| 1168 |
-
hoveredBarKey = null;
|
| 1169 |
-
hideTooltip();
|
| 1170 |
-
}}
|
| 1171 |
/>
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
>
|
| 1179 |
-
{#each barLabelLines(groupLabel(group)) as line, lineIndex (`${group}-${lineIndex}`)}
|
| 1180 |
-
<tspan x="0" dy={lineIndex === 0 ? 0 : 14}>{line}</tspan>
|
| 1181 |
{/each}
|
| 1182 |
-
</text>
|
| 1183 |
-
{/each}
|
| 1184 |
-
</svg>
|
| 1185 |
-
</div>
|
| 1186 |
-
<div class="mt-3 flex flex-wrap gap-4 text-xs text-muted-foreground">
|
| 1187 |
-
<span class="inline-flex items-center gap-2"
|
| 1188 |
-
><span class="size-2 rounded-full bg-[#ff4d4f]"></span>FNR ({m.tools_lower_better()})</span
|
| 1189 |
-
>
|
| 1190 |
-
<span class="inline-flex items-center gap-2"
|
| 1191 |
-
><span class="size-2 rounded-full bg-[#f5a623]"></span>FPR ({m.tools_lower_better()})</span
|
| 1192 |
-
>
|
| 1193 |
-
<span class="inline-flex items-center gap-2"
|
| 1194 |
-
><span class="size-2 rounded-full bg-[#0dce97]"></span>F1 ({m.tools_higher_better()})</span
|
| 1195 |
-
>
|
| 1196 |
-
</div>
|
| 1197 |
-
</section>
|
| 1198 |
-
|
| 1199 |
-
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1200 |
-
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 1201 |
-
<h2 class="font-mono text-sm font-semibold">{m.tools_pareto_title()}</h2>
|
| 1202 |
-
<span class="text-xs text-muted-foreground">{m.tools_pareto_hint()}</span>
|
| 1203 |
-
</div>
|
| 1204 |
-
<div class="max-w-full overflow-x-auto">
|
| 1205 |
-
<svg
|
| 1206 |
-
viewBox="0 0 760 380"
|
| 1207 |
-
class="h-auto min-w-[42rem] w-full"
|
| 1208 |
-
role="img"
|
| 1209 |
-
aria-label={m.tools_pareto_aria()}
|
| 1210 |
-
>
|
| 1211 |
-
{#each paretoXTicks as xt (xt)}
|
| 1212 |
-
{#if xt > 0}
|
| 1213 |
<line
|
| 1214 |
-
x1=
|
| 1215 |
y1="18"
|
| 1216 |
-
x2=
|
| 1217 |
y2="330"
|
| 1218 |
stroke="currentColor"
|
| 1219 |
stroke-width="1"
|
| 1220 |
-
stroke-dasharray="3 4"
|
| 1221 |
class="text-border"
|
| 1222 |
/>
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
|
| 1233 |
-
|
| 1234 |
-
y1={paretoY(yt, paretoY0)}
|
| 1235 |
-
x2="742"
|
| 1236 |
-
y2={paretoY(yt, paretoY0)}
|
| 1237 |
-
stroke="currentColor"
|
| 1238 |
-
stroke-width="1"
|
| 1239 |
-
stroke-dasharray="3 4"
|
| 1240 |
-
class="text-border"
|
| 1241 |
-
/>
|
| 1242 |
-
<text
|
| 1243 |
-
x="48"
|
| 1244 |
-
y={paretoY(yt, paretoY0) + 3}
|
| 1245 |
-
text-anchor="end"
|
| 1246 |
-
class="fill-muted-foreground font-mono text-[7px]">{yt.toFixed(2)}</text
|
| 1247 |
-
>
|
| 1248 |
-
{/each}
|
| 1249 |
-
<line
|
| 1250 |
-
x1="56"
|
| 1251 |
-
y1="18"
|
| 1252 |
-
x2="56"
|
| 1253 |
-
y2="330"
|
| 1254 |
-
stroke="currentColor"
|
| 1255 |
-
stroke-width="1"
|
| 1256 |
-
class="text-border"
|
| 1257 |
-
/>
|
| 1258 |
-
<line
|
| 1259 |
-
x1="56"
|
| 1260 |
-
y1="330"
|
| 1261 |
-
x2="742"
|
| 1262 |
-
y2="330"
|
| 1263 |
-
stroke="currentColor"
|
| 1264 |
-
stroke-width="1"
|
| 1265 |
-
class="text-border"
|
| 1266 |
-
/>
|
| 1267 |
-
<polyline
|
| 1268 |
-
points={paretoFrontFor(paretoRows, paretoXMax, paretoY0)}
|
| 1269 |
-
fill="none"
|
| 1270 |
-
stroke="#6b4bf5"
|
| 1271 |
-
stroke-width="1.6"
|
| 1272 |
-
stroke-dasharray="5 4"
|
| 1273 |
-
/>
|
| 1274 |
-
{#each paretoRows as row (row.modelKey)}
|
| 1275 |
-
{@const point = paretoPointFor(row, paretoXMax, paretoY0)}
|
| 1276 |
-
{@const pinned = pinnedModelKeys.includes(row.modelKey)}
|
| 1277 |
-
{@const hovered = hoveredParetoModelKey === row.modelKey}
|
| 1278 |
-
<circle
|
| 1279 |
-
cx={point.x}
|
| 1280 |
-
cy={point.y}
|
| 1281 |
-
r={hovered ? 8 : pinned ? 7 : 5.5}
|
| 1282 |
-
fill={heatColor(clamp01((row.fpr ?? 0) / 0.15))}
|
| 1283 |
-
stroke="currentColor"
|
| 1284 |
-
stroke-width={pinned ? 2 : 1.5}
|
| 1285 |
-
class="cursor-pointer text-background transition-[r] focus:outline-none"
|
| 1286 |
-
role="button"
|
| 1287 |
-
tabindex="0"
|
| 1288 |
-
aria-pressed={pinned}
|
| 1289 |
-
aria-label={pinned
|
| 1290 |
-
? m.tools_pareto_unpin_model({ model: row.model.short })
|
| 1291 |
-
: m.tools_pareto_pin_model({ model: row.model.short })}
|
| 1292 |
-
onclick={() => togglePin(row.modelKey)}
|
| 1293 |
-
onkeydown={(event) => handleCheckboxKeydown(event, () => togglePin(row.modelKey))}
|
| 1294 |
-
onfocus={() => (hoveredParetoModelKey = row.modelKey)}
|
| 1295 |
-
onblur={() => (hoveredParetoModelKey = null)}
|
| 1296 |
-
onpointerenter={(event) => {
|
| 1297 |
-
hoveredParetoModelKey = row.modelKey;
|
| 1298 |
-
updateTooltip(event, row.model.short, [
|
| 1299 |
-
{ label: 'integral', value: fmt(row.integral, 3) },
|
| 1300 |
-
{ label: 'p95', value: `${fmtMs(row.p95)} ${m.ranking_unit_ms()}` },
|
| 1301 |
-
{ label: 'FPR', value: fmt(row.fpr, 3) }
|
| 1302 |
-
]);
|
| 1303 |
-
}}
|
| 1304 |
-
onpointermove={moveTooltip}
|
| 1305 |
-
onpointerleave={() => {
|
| 1306 |
-
hoveredParetoModelKey = null;
|
| 1307 |
-
hideTooltip();
|
| 1308 |
-
}}
|
| 1309 |
-
/>
|
| 1310 |
-
{#if pinned}
|
| 1311 |
-
<circle
|
| 1312 |
-
cx={point.x}
|
| 1313 |
-
cy={point.y}
|
| 1314 |
-
r="9.5"
|
| 1315 |
fill="none"
|
| 1316 |
stroke="#6b4bf5"
|
| 1317 |
-
stroke-width="1.
|
| 1318 |
-
|
| 1319 |
/>
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1328 |
<text
|
| 1329 |
-
x=
|
| 1330 |
-
y=
|
| 1331 |
text-anchor="middle"
|
| 1332 |
-
|
| 1333 |
-
stroke-width="4"
|
| 1334 |
-
stroke-linejoin="round"
|
| 1335 |
-
paint-order="stroke"
|
| 1336 |
-
class="fill-muted-foreground font-mono text-[8px]"
|
| 1337 |
>
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
>
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1366 |
-
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 1367 |
-
<h2 class="font-mono text-sm font-semibold">
|
| 1368 |
-
{m.tools_latency_title()}
|
| 1369 |
-
</h2>
|
| 1370 |
-
<span class="text-xs text-muted-foreground">{m.tools_latency_hint()}</span>
|
| 1371 |
-
</div>
|
| 1372 |
-
<Table
|
| 1373 |
-
data={perfRows}
|
| 1374 |
-
columns={latencyColumns}
|
| 1375 |
-
getRowKey={(row) => row.modelKey}
|
| 1376 |
-
containerClass="max-w-full overflow-x-auto rounded-lg border"
|
| 1377 |
-
tableClass="min-w-[50rem]"
|
| 1378 |
-
headerClass="bg-muted/70 hover:bg-muted/70 [&_th]:text-muted-foreground"
|
| 1379 |
-
rowClass="hover:bg-muted/30"
|
| 1380 |
-
>
|
| 1381 |
-
{#snippet header(column)}
|
| 1382 |
-
<Button
|
| 1383 |
-
variant="ghost"
|
| 1384 |
-
size="sm"
|
| 1385 |
-
class={cn(
|
| 1386 |
-
'h-auto gap-1 px-0 py-0 text-xs font-semibold text-muted-foreground hover:bg-transparent',
|
| 1387 |
-
column.key !== 'model' && 'ml-auto'
|
| 1388 |
-
)}
|
| 1389 |
-
onclick={() => toggleLatencySort(column.key)}
|
| 1390 |
>
|
| 1391 |
-
{
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
</Table>
|
| 1410 |
-
<p class="mt-3 text-xs text-muted-foreground">{m.tools_latency_note()}</p>
|
| 1411 |
-
</section>
|
| 1412 |
-
|
| 1413 |
-
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1414 |
-
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 1415 |
-
<h2 class="font-mono text-sm font-semibold">
|
| 1416 |
-
{m.tools_robustness_title()}
|
| 1417 |
-
</h2>
|
| 1418 |
-
<span class="text-xs text-muted-foreground">{m.tools_robustness_hint()}</span>
|
| 1419 |
-
</div>
|
| 1420 |
-
<p class="mb-3 text-xs leading-5 text-muted-foreground">{m.tools_robustness_note()}</p>
|
| 1421 |
-
<div class="max-w-full overflow-x-auto rounded-lg border">
|
| 1422 |
-
<table class="w-full min-w-[54rem] text-sm">
|
| 1423 |
-
<thead class="bg-muted/70 text-xs text-muted-foreground">
|
| 1424 |
-
<tr>
|
| 1425 |
-
<th class="px-3 py-3 text-left font-semibold">{m.ranking_column_model()}</th>
|
| 1426 |
-
<th class="px-3 py-3 text-right font-semibold">{m.tools_real_score()}</th>
|
| 1427 |
-
<th class="px-3 py-3 text-right font-semibold">{m.tools_robust_score()}</th>
|
| 1428 |
-
<th class="px-3 py-3 text-right font-semibold">Δ score</th>
|
| 1429 |
-
<th class="px-3 py-3 text-right font-semibold">FNR real</th>
|
| 1430 |
-
<th class="px-3 py-3 text-right font-semibold">FNR robust</th>
|
| 1431 |
-
<th class="px-3 py-3 text-right font-semibold">Δ FNR</th>
|
| 1432 |
-
</tr>
|
| 1433 |
-
</thead>
|
| 1434 |
-
<tbody>
|
| 1435 |
-
{#each robustRows as row (row.modelKey)}
|
| 1436 |
-
{@const sDelta = row.deltaScore}
|
| 1437 |
-
{@const fDelta = row.deltaFnr}
|
| 1438 |
-
<tr class="border-t transition-colors hover:bg-muted/30">
|
| 1439 |
-
<td class="px-3 py-2">
|
| 1440 |
-
<div class="font-mono text-xs font-semibold">
|
| 1441 |
-
<span
|
| 1442 |
-
class="mr-2 inline-block size-2 rounded-full"
|
| 1443 |
-
style={`background:${colorOf(row.model)}`}
|
| 1444 |
-
></span>
|
| 1445 |
-
{row.model.short}
|
| 1446 |
-
<code class="text-muted-foreground">{row.model.params}</code>
|
| 1447 |
-
</div>
|
| 1448 |
-
{#if row.category}
|
| 1449 |
-
<div class="mt-1 text-xs text-muted-foreground">{row.category}</div>
|
| 1450 |
{/if}
|
| 1451 |
-
|
| 1452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1453 |
<span
|
| 1454 |
-
class=
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1459 |
<span
|
| 1460 |
-
class=
|
| 1461 |
-
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
| 1469 |
-
|
| 1470 |
-
class=
|
| 1471 |
-
|
| 1472 |
-
|
| 1473 |
-
|
| 1474 |
-
|
| 1475 |
-
|
| 1476 |
-
|
| 1477 |
-
|
| 1478 |
-
|
| 1479 |
-
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
| 1484 |
-
deltaBadgeClass(fDelta, 'fnr')
|
| 1485 |
-
)}
|
| 1486 |
-
>
|
| 1487 |
-
{fmtDelta(fDelta)}
|
| 1488 |
-
</span>
|
| 1489 |
-
</td>
|
| 1490 |
-
</tr>
|
| 1491 |
-
{/each}
|
| 1492 |
-
</tbody>
|
| 1493 |
-
</table>
|
| 1494 |
-
</div>
|
| 1495 |
-
</section>
|
| 1496 |
-
</div>
|
| 1497 |
-
|
| 1498 |
-
<p class="mt-8 text-center text-xs text-muted-foreground">
|
| 1499 |
-
{m.tools_footer()}
|
| 1500 |
-
</p>
|
| 1501 |
-
|
| 1502 |
-
{#if chartTooltip}
|
| 1503 |
-
<div
|
| 1504 |
-
bind:this={chartTooltipElement}
|
| 1505 |
-
class="pointer-events-none fixed z-50 min-w-48 max-w-[min(24rem,calc(100vw-1.5rem))] rounded-lg border bg-popover px-4 py-3 text-sm text-popover-foreground shadow-xl"
|
| 1506 |
-
style={`left:${chartTooltip.x}px; top:${chartTooltip.y}px`}
|
| 1507 |
-
>
|
| 1508 |
-
<div class="break-words font-semibold">{chartTooltip.title}</div>
|
| 1509 |
-
<div class="mt-2 space-y-1 font-mono">
|
| 1510 |
-
{#each chartTooltip.rows as row (`${row.label}-${row.value}`)}
|
| 1511 |
-
<div class="grid grid-cols-[auto_1fr] gap-5">
|
| 1512 |
-
<span class="break-words text-muted-foreground">{row.label}</span>
|
| 1513 |
-
<span class="text-right">{row.value}</span>
|
| 1514 |
</div>
|
| 1515 |
-
|
| 1516 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1517 |
</div>
|
| 1518 |
-
{/
|
| 1519 |
-
</
|
| 1520 |
{/if}
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import {
|
| 3 |
+
ArrowDownIcon,
|
| 4 |
+
ArrowUpDownIcon,
|
| 5 |
+
ArrowUpIcon,
|
| 6 |
+
DownloadIcon,
|
| 7 |
+
PinIcon,
|
| 8 |
+
XIcon
|
| 9 |
+
} from '@lucide/svelte';
|
| 10 |
import BucketDataFallback from '$lib/components/BucketDataFallback.svelte';
|
| 11 |
import BucketDataWarning from '$lib/components/BucketDataWarning.svelte';
|
| 12 |
+
import RadarLabelsOverlay from '$lib/components/charts/RadarLabelsOverlay.svelte';
|
| 13 |
import PageHeadingSection from '$lib/components/PageHeadingSection.svelte';
|
| 14 |
import Select from '$lib/components/Select.svelte';
|
| 15 |
import Table, { type TableColumn } from '$lib/components/Table.svelte';
|
| 16 |
+
import ChartCursorTooltip from '$lib/components/tooltips/ChartCursorTooltip.svelte';
|
| 17 |
+
import OverflowCursorTooltip from '$lib/components/tooltips/OverflowCursorTooltip.svelte';
|
| 18 |
import { Badge } from '$lib/components/ui/badge';
|
| 19 |
import { Button } from '$lib/components/ui/button';
|
| 20 |
import { Checkbox } from '$lib/components/ui/checkbox';
|
| 21 |
import {
|
| 22 |
toolsActiveGroups,
|
|
|
|
| 23 |
toolsBarY as barY,
|
| 24 |
toolsCsvDataUri,
|
| 25 |
toolsDataGroups,
|
|
|
|
| 39 |
import { isNumber } from '$lib/helpers/leaderboard';
|
| 40 |
import type { GuardModel } from '$lib/types/leaderboard';
|
| 41 |
import type {
|
|
|
|
| 42 |
ToolsLanguageFilter as LangFilter,
|
| 43 |
ToolsLatencySortKey as LatencySortKey,
|
| 44 |
ToolsRadarScale as RadarScale,
|
| 45 |
ToolsSortDirection as SortDirection,
|
|
|
|
| 46 |
ToolsVizMetric as VizMetric
|
| 47 |
} from '$lib/types/tools';
|
| 48 |
import { m } from '$lib/paraglide/messages';
|
|
|
|
| 65 |
type ParetoRow = VisualizationModelRow & {
|
| 66 |
p95: number;
|
| 67 |
integral: number;
|
|
|
|
| 68 |
};
|
| 69 |
type PerformanceRow = VisualizationModelRow & {
|
| 70 |
rank: number;
|
|
|
|
| 81 |
fnrReal: number | null;
|
| 82 |
fnrRobust: number | null;
|
| 83 |
deltaFnr: number | null;
|
| 84 |
+
fprReal: number | null;
|
| 85 |
+
fprRobust: number | null;
|
| 86 |
+
deltaFpr: number | null;
|
| 87 |
category: string;
|
| 88 |
};
|
| 89 |
type HeatmapRow = ToolsDataset['visualizations']['heatmap']['rows'][number];
|
|
|
|
| 92 |
max: number | null;
|
| 93 |
};
|
| 94 |
const selectContentClass = 'max-h-72';
|
| 95 |
+
const radarViewBoxWidth = 520;
|
| 96 |
+
const radarViewBoxHeight = 420;
|
| 97 |
+
const barModelBandWidth = 145;
|
| 98 |
+
const barSeriesWidth = 26;
|
| 99 |
+
const barSeriesGap = 3;
|
| 100 |
+
const barGroupWidth = barSeriesWidth * 3 + barSeriesGap * 2;
|
| 101 |
+
const scatterAxisMax = 1;
|
| 102 |
+
const scatterTicks = [0, 0.25, 0.5, 0.75, 1];
|
| 103 |
let { data }: PageProps = $props();
|
| 104 |
const toolsState = $derived(data.toolsState);
|
| 105 |
const tools = $derived(toolsState.data);
|
|
|
|
| 110 |
const initialTools = (() => data.toolsState.data)();
|
| 111 |
const initialGroups = toolsDataGroups(initialTools.models, initialTools.allGroups);
|
| 112 |
|
| 113 |
+
let selectedModelKeys = $state(initialTools.models.slice(0, 5).map((model) => modelKey(model)));
|
| 114 |
let selectedGroups = $state<string[]>(initialGroups);
|
| 115 |
let language = $state<LangFilter>('all');
|
| 116 |
let metric = $state<VizMetric>('fnr');
|
|
|
|
| 118 |
let pinnedModelKeys = $state<string[]>([]);
|
| 119 |
let latencySortKey = $state<LatencySortKey>('p95');
|
| 120 |
let latencySortDirection = $state<SortDirection>('asc');
|
|
|
|
|
|
|
| 121 |
let hoveredRadarModelKey = $state<string | null>(null);
|
| 122 |
let hoveredScatterModelKey = $state<string | null>(null);
|
| 123 |
let hoveredBarKey = $state<string | null>(null);
|
| 124 |
let hoveredParetoModelKey = $state<string | null>(null);
|
| 125 |
+
let barContainerWidth = $state(0);
|
| 126 |
|
| 127 |
const languageOptions = [
|
| 128 |
{ value: 'all', label: m.tools_language_all() },
|
|
|
|
| 138 |
{ value: 'f1', label: 'F1' }
|
| 139 |
];
|
| 140 |
const latencyColumns: LatencyColumn[] = [
|
| 141 |
+
{
|
| 142 |
+
key: 'model',
|
| 143 |
+
label: m.ranking_column_model,
|
| 144 |
+
headClass: () => latencySortColumnClass('model'),
|
| 145 |
+
cellClass: () => latencySortCellClass('model')
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
key: 'p50',
|
| 149 |
+
label: () => `p50 (${m.ranking_unit_ms()})`,
|
| 150 |
+
headClass: () => cn('text-right', latencySortColumnClass('p50')),
|
| 151 |
+
cellClass: () => cn('text-right', latencySortCellClass('p50'))
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
key: 'p95',
|
| 155 |
+
label: () => `p95 (${m.ranking_unit_ms()})`,
|
| 156 |
+
headClass: () => cn('text-right', latencySortColumnClass('p95')),
|
| 157 |
+
cellClass: () => cn('text-right', latencySortCellClass('p95'))
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
key: 'p99',
|
| 161 |
+
label: () => `p99 (${m.ranking_unit_ms()})`,
|
| 162 |
+
headClass: () => cn('text-right', latencySortColumnClass('p99')),
|
| 163 |
+
cellClass: () => cn('text-right', latencySortCellClass('p99'))
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
key: 'errorRate',
|
| 167 |
+
label: m.details_error_rate,
|
| 168 |
+
headClass: () => cn('text-right', latencySortColumnClass('errorRate')),
|
| 169 |
+
cellClass: () => cn('text-right', latencySortCellClass('errorRate'))
|
| 170 |
+
}
|
| 171 |
];
|
| 172 |
|
| 173 |
const currentLanguageLabel = $derived(
|
|
|
|
| 198 |
const heatColumns = $derived(
|
| 199 |
`minmax(8rem, 10rem) repeat(${heatmapModels.length}, minmax(4rem, 1fr))`
|
| 200 |
);
|
| 201 |
+
const heatmapHasData = $derived(heatmapModels.length > 0 && heatmapRows.length > 0);
|
| 202 |
const scatterRows = $derived(
|
| 203 |
scatterRowsFor(tools.visualizations.scatter.points, modelByKey, selectedModelKeySet)
|
| 204 |
);
|
|
|
|
| 222 |
const robustRows = $derived(
|
| 223 |
robustnessRowsFor(tools.visualizations.robustness.rows, modelByKey, selectedModelKeySet)
|
| 224 |
);
|
| 225 |
+
const scatterXMax = scatterAxisMax;
|
| 226 |
+
const scatterYMax = scatterAxisMax;
|
| 227 |
+
const barSvgWidth = $derived(
|
| 228 |
+
Math.max(720, barContainerWidth, selectedModels.length * barModelBandWidth + 56)
|
| 229 |
+
);
|
| 230 |
const paretoXMax = $derived(Math.max(1, ...paretoRows.map((row) => row.p95)) * 1.08);
|
| 231 |
const paretoY0 = $derived(paretoYStart(paretoRows));
|
| 232 |
const paretoXTicks = $derived(niceTicks(0, paretoXMax, 6));
|
|
|
|
| 257 |
return `${Math.round(clamp01(value ?? 0) * 100)}%`;
|
| 258 |
}
|
| 259 |
|
| 260 |
+
function modelLabel(model: GuardModel) {
|
| 261 |
+
return `${model.short} ${model.params}`;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
function scatterRowsFor(
|
| 265 |
points: ToolsDataset['visualizations']['scatter']['points'],
|
| 266 |
modelsByKey: ReadonlyMap<string, GuardModel>,
|
|
|
|
| 297 |
) {
|
| 298 |
return [];
|
| 299 |
}
|
| 300 |
+
return [{ modelKey: key, model, p95: row.latencyMs.p95, integral: row.integral }];
|
|
|
|
|
|
|
| 301 |
});
|
| 302 |
}
|
| 303 |
|
|
|
|
| 357 |
fnrReal: row.fnrReal,
|
| 358 |
fnrRobust: row.fnrRobust,
|
| 359 |
deltaFnr: row.deltaFnr,
|
| 360 |
+
fprReal: row.fprReal,
|
| 361 |
+
fprRobust: row.fprRobust,
|
| 362 |
+
deltaFpr: row.deltaFpr,
|
| 363 |
category: localized(row.categoryLabel, '')
|
| 364 |
}
|
| 365 |
];
|
|
|
|
| 388 |
}
|
| 389 |
|
| 390 |
function groupedBarsMean(
|
| 391 |
+
modelKeyValue: string,
|
| 392 |
key: VizMetric,
|
| 393 |
modelsByVisualizationKey: ReadonlyMap<
|
| 394 |
string,
|
| 395 |
ToolsDataset['visualizations']['groupedBars']['models'][number]
|
| 396 |
>,
|
| 397 |
+
groups: readonly string[]
|
| 398 |
) {
|
| 399 |
+
const model = modelsByVisualizationKey.get(modelKeyValue);
|
| 400 |
+
const values = groups.map((group) => model?.groups[group]?.[key]).filter(isNumber);
|
|
|
|
| 401 |
|
| 402 |
if (!values.length) return null;
|
| 403 |
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
|
|
| 422 |
}
|
| 423 |
|
| 424 |
function paretoLabelText(row: ParetoRow) {
|
| 425 |
+
const label = modelLabel(row.model);
|
| 426 |
+
return label.length > 18 ? `${label.slice(0, 17)}…` : label;
|
| 427 |
}
|
| 428 |
|
| 429 |
function paretoYStart(rows: readonly ParetoRow[]) {
|
|
|
|
| 478 |
action();
|
| 479 |
}
|
| 480 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
function toggleLatencySort(key: LatencySortKey) {
|
| 482 |
if (latencySortKey === key) {
|
| 483 |
latencySortDirection = latencySortDirection === 'asc' ? 'desc' : 'asc';
|
|
|
|
| 492 |
return typeof column.label === 'function' ? column.label() : column.label;
|
| 493 |
}
|
| 494 |
|
| 495 |
+
function latencySortColumnClass(key: LatencySortKey) {
|
| 496 |
+
return latencySortKey === key ? 'bg-muted/80 text-foreground' : undefined;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
function latencySortCellClass(key: LatencySortKey) {
|
| 500 |
+
return latencySortKey === key ? 'bg-muted/20 text-foreground' : undefined;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
function heatTextColor(value: number | null | undefined) {
|
| 504 |
if (!isNumber(value)) return 'text-muted-foreground';
|
| 505 |
return 'text-black';
|
| 506 |
}
|
| 507 |
|
| 508 |
+
function deltaBadgeClass(value: number | null, kind: 'score' | 'fnr' | 'fpr') {
|
| 509 |
if (!isNumber(value) || value === 0) {
|
| 510 |
return 'bg-muted/40 text-muted-foreground';
|
| 511 |
}
|
|
|
|
| 531 |
<BucketDataFallback />
|
| 532 |
</div>
|
| 533 |
{:else}
|
| 534 |
+
<ChartCursorTooltip class="min-w-48">
|
| 535 |
+
{#snippet children(tooltip)}
|
| 536 |
+
<div class="mx-auto flex w-full min-w-0 max-w-7xl flex-col px-4 py-8 sm:px-6 lg:px-8">
|
| 537 |
+
{#if toolsState.status === 'stale'}
|
| 538 |
+
<BucketDataWarning />
|
| 539 |
+
{/if}
|
| 540 |
+
<PageHeadingSection
|
| 541 |
+
title={m.navigation_visualizations()}
|
| 542 |
+
description={m.tools_description()}
|
| 543 |
+
/>
|
| 544 |
+
|
| 545 |
+
<section
|
| 546 |
+
class="mt-6 min-w-0 rounded-xl border bg-card p-4 shadow-sm"
|
| 547 |
+
aria-label={m.tools_controls_aria()}
|
| 548 |
+
>
|
| 549 |
+
<div class="grid min-w-0 gap-4 lg:grid-cols-[1.1fr_1.1fr_.7fr]">
|
| 550 |
+
<div class="min-w-0">
|
| 551 |
+
<div class="mb-2 flex items-center justify-between gap-3">
|
| 552 |
+
<h2 class="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
| 553 |
+
{m.tools_models_title()}
|
| 554 |
+
</h2>
|
| 555 |
+
<div class="flex gap-2 text-xs">
|
| 556 |
+
<Button
|
| 557 |
+
variant="outline"
|
| 558 |
+
size="sm"
|
| 559 |
+
class="h-8 rounded-full px-4"
|
| 560 |
+
onclick={() => (selectedModelKeys = models.map((model) => modelKey(model)))}
|
| 561 |
+
>{m.details_select_all()}</Button
|
| 562 |
+
>
|
| 563 |
+
<Button
|
| 564 |
+
variant="outline"
|
| 565 |
+
size="sm"
|
| 566 |
+
class="h-8 rounded-full px-4"
|
| 567 |
+
onclick={() => (selectedModelKeys = [])}>{m.details_reset()}</Button
|
| 568 |
+
>
|
| 569 |
+
</div>
|
| 570 |
+
</div>
|
| 571 |
+
<div class="max-h-36 space-y-1 overflow-auto rounded-lg border bg-background/50 p-2">
|
| 572 |
+
{#each models as model (modelKey(model))}
|
| 573 |
+
{@const key = modelKey(model)}
|
| 574 |
+
<div
|
| 575 |
+
class="flex min-w-0 items-center gap-3 rounded-md px-2 py-1.5 text-sm hover:bg-muted/50"
|
| 576 |
+
role="checkbox"
|
| 577 |
+
tabindex="0"
|
| 578 |
+
aria-checked={selectedModelKeys.includes(key)}
|
| 579 |
+
onclick={() => (selectedModelKeys = toggleListValue(selectedModelKeys, key))}
|
| 580 |
+
onkeydown={(event) =>
|
| 581 |
+
handleCheckboxKeydown(
|
| 582 |
+
event,
|
| 583 |
+
() => (selectedModelKeys = toggleListValue(selectedModelKeys, key))
|
| 584 |
+
)}
|
| 585 |
+
>
|
| 586 |
+
<Checkbox
|
| 587 |
+
checked={selectedModelKeys.includes(key)}
|
| 588 |
+
aria-label={modelLabel(model)}
|
| 589 |
+
tabindex={-1}
|
| 590 |
+
/>
|
| 591 |
+
<span class="size-3 rounded-sm" style={`background:${colorOf(model)}`}></span>
|
| 592 |
+
<span class="min-w-0 flex-1 truncate">
|
| 593 |
+
{model.short} <code class="text-muted-foreground">{model.params}</code>
|
| 594 |
+
</span>
|
| 595 |
+
<Button
|
| 596 |
+
variant="outline"
|
| 597 |
+
size="sm"
|
| 598 |
+
class={cn(
|
| 599 |
+
'h-7 rounded-full px-3 text-xs',
|
| 600 |
+
pinnedModelKeys.includes(key) &&
|
| 601 |
+
'border-[#0dce97]/60 bg-[#0dce97]/10 text-[#0dce97] hover:bg-[#0dce97]/15 hover:text-[#0dce97]'
|
| 602 |
+
)}
|
| 603 |
+
aria-pressed={pinnedModelKeys.includes(key)}
|
| 604 |
+
onclick={(event) => {
|
| 605 |
+
event.preventDefault();
|
| 606 |
+
event.stopPropagation();
|
| 607 |
+
togglePin(key);
|
| 608 |
+
}}
|
| 609 |
+
>
|
| 610 |
+
<PinIcon class="size-3" />
|
| 611 |
+
{pinnedModelKeys.includes(key) ? m.tools_pinned() : m.tools_pin()}
|
| 612 |
+
</Button>
|
| 613 |
+
</div>
|
| 614 |
+
{/each}
|
| 615 |
+
</div>
|
| 616 |
+
</div>
|
| 617 |
|
| 618 |
+
<div class="min-w-0">
|
| 619 |
+
<div class="mb-2 flex items-center justify-between gap-3">
|
| 620 |
+
<h2 class="text-xs font-semibold tracking-wider text-muted-foreground uppercase">
|
| 621 |
+
{m.tools_groups_title()}
|
| 622 |
+
</h2>
|
| 623 |
+
<div class="flex gap-2 text-xs">
|
| 624 |
+
<Button
|
| 625 |
+
variant="outline"
|
| 626 |
+
size="sm"
|
| 627 |
+
class="h-8 rounded-full px-4"
|
| 628 |
+
onclick={() => (selectedGroups = [...dataGroups])}
|
| 629 |
+
>{m.details_select_all()}</Button
|
| 630 |
+
>
|
| 631 |
+
<Button
|
| 632 |
+
variant="outline"
|
| 633 |
+
size="sm"
|
| 634 |
+
class="h-8 rounded-full px-4"
|
| 635 |
+
onclick={() => (selectedGroups = [])}>{m.details_reset()}</Button
|
| 636 |
+
>
|
| 637 |
+
</div>
|
| 638 |
+
</div>
|
| 639 |
+
<div class="max-h-36 space-y-1 overflow-auto rounded-lg border bg-background/50 p-2">
|
| 640 |
+
{#each dataGroups as group (group)}
|
| 641 |
+
<div
|
| 642 |
+
class="flex min-w-0 items-center gap-3 rounded-md px-2 py-1.5 text-sm hover:bg-muted/50"
|
| 643 |
+
role="checkbox"
|
| 644 |
+
tabindex="0"
|
| 645 |
+
aria-checked={selectedGroups.includes(group)}
|
| 646 |
+
onclick={() => (selectedGroups = toggleListValue(selectedGroups, group))}
|
| 647 |
+
onkeydown={(event) =>
|
| 648 |
+
handleCheckboxKeydown(
|
| 649 |
+
event,
|
| 650 |
+
() => (selectedGroups = toggleListValue(selectedGroups, group))
|
| 651 |
+
)}
|
| 652 |
+
>
|
| 653 |
+
<Checkbox
|
| 654 |
+
checked={selectedGroups.includes(group)}
|
| 655 |
+
aria-label={groupLabel(group)}
|
| 656 |
+
tabindex={-1}
|
| 657 |
+
/>
|
| 658 |
+
<span class="min-w-0 truncate">
|
| 659 |
+
{groupLabel(group)}
|
| 660 |
+
<span class="text-muted-foreground"
|
| 661 |
+
>{benchmarkMeta[group]?.languages.join(' · ')}</span
|
| 662 |
+
>
|
| 663 |
+
</span>
|
| 664 |
+
</div>
|
| 665 |
+
{/each}
|
| 666 |
+
</div>
|
| 667 |
+
</div>
|
| 668 |
+
|
| 669 |
+
<div class="grid content-start gap-3">
|
| 670 |
+
<Select
|
| 671 |
+
id="tools-language"
|
| 672 |
+
type="single"
|
| 673 |
+
bind:value={language}
|
| 674 |
+
options={languageOptions}
|
| 675 |
+
label={m.tools_language_label()}
|
| 676 |
+
triggerLabel={currentLanguageLabel}
|
| 677 |
+
contentClass={selectContentClass}
|
| 678 |
+
/>
|
| 679 |
+
<Select
|
| 680 |
+
id="tools-metric"
|
| 681 |
+
type="single"
|
| 682 |
+
bind:value={metric}
|
| 683 |
+
options={metricOptions}
|
| 684 |
+
label={m.tools_metric_label()}
|
| 685 |
+
triggerLabel={currentMetricLabel}
|
| 686 |
+
contentClass={selectContentClass}
|
| 687 |
+
/>
|
| 688 |
</div>
|
| 689 |
</div>
|
| 690 |
+
|
| 691 |
+
<div class="mt-4 flex flex-wrap items-end gap-3">
|
| 692 |
+
<div class="min-w-0 space-y-2">
|
| 693 |
+
<span
|
| 694 |
+
class="block text-xs font-semibold tracking-wider text-muted-foreground uppercase"
|
| 695 |
+
>{m.tools_radar_scale()}</span
|
| 696 |
+
>
|
| 697 |
<div
|
| 698 |
+
class="inline-flex max-w-full flex-wrap gap-1 rounded-4xl border bg-input/50 p-1"
|
| 699 |
+
role="group"
|
| 700 |
+
aria-label={m.tools_radar_scale()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 701 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
<Button
|
| 703 |
+
variant={radarScale === 'score' ? 'default' : 'ghost'}
|
| 704 |
size="sm"
|
| 705 |
+
aria-pressed={radarScale === 'score'}
|
| 706 |
+
onclick={() => (radarScale = 'score')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
>
|
| 708 |
+
{m.tools_radar_group_score()}
|
| 709 |
+
</Button>
|
| 710 |
+
<Button
|
| 711 |
+
variant={radarScale === 'f1' ? 'default' : 'ghost'}
|
| 712 |
+
size="sm"
|
| 713 |
+
aria-pressed={radarScale === 'f1'}
|
| 714 |
+
onclick={() => (radarScale = 'f1')}
|
| 715 |
+
>
|
| 716 |
+
F1
|
| 717 |
</Button>
|
| 718 |
</div>
|
| 719 |
+
</div>
|
| 720 |
+
<div class="ml-auto flex flex-wrap items-center justify-end gap-3">
|
| 721 |
+
<span
|
| 722 |
+
class="rounded-full border border-[#0dce97]/40 bg-[#0dce97]/10 px-4 py-2 font-mono text-xs font-semibold text-[#0dce97]"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 723 |
>
|
| 724 |
+
{m.tools_selected_summary({
|
| 725 |
+
models: selectedModels.length,
|
| 726 |
+
groups: activeGroups.length
|
| 727 |
+
})}
|
| 728 |
+
</span>
|
| 729 |
<Button
|
| 730 |
+
href={csvHref}
|
| 731 |
+
download="hivetrace_tools_selection.csv"
|
| 732 |
size="sm"
|
| 733 |
+
class="bg-[#0dce97] text-emerald-950 hover:bg-[#0dce97]/90"
|
|
|
|
| 734 |
>
|
| 735 |
+
<DownloadIcon data-icon="inline-start" />
|
| 736 |
+
{m.ranking_csv()}
|
| 737 |
+
</Button>
|
| 738 |
</div>
|
| 739 |
</div>
|
| 740 |
+
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
|
| 742 |
+
<div
|
| 743 |
+
class="mt-4 flex min-h-[46px] flex-wrap items-center gap-x-2 gap-y-3 rounded-xl border bg-card px-4 py-3 text-xs leading-none"
|
| 744 |
+
>
|
| 745 |
+
<span class="font-semibold tracking-wider text-muted-foreground uppercase leading-none"
|
| 746 |
+
>{m.tools_pinned_for_comparison()}</span
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 747 |
>
|
| 748 |
+
{#if pinnedModels.length}
|
| 749 |
+
{#each pinnedModels as model (modelKey(model))}
|
| 750 |
+
<Badge variant="secondary" class="gap-1 rounded-full">
|
| 751 |
+
<span class="size-2 rounded-full" style={`background:${colorOf(model)}`}></span>
|
| 752 |
+
{modelLabel(model)}
|
| 753 |
+
<button
|
| 754 |
+
type="button"
|
| 755 |
+
aria-label={m.tools_unpin_model({ model: modelLabel(model) })}
|
| 756 |
+
onclick={() => togglePin(modelKey(model))}
|
| 757 |
+
>
|
| 758 |
+
<XIcon class="size-3" />
|
| 759 |
+
</button>
|
| 760 |
+
</Badge>
|
| 761 |
+
{/each}
|
| 762 |
+
{:else}
|
| 763 |
+
<span class="leading-none text-muted-foreground">
|
| 764 |
+
{m.tools_no_pinned()}
|
| 765 |
+
</span>
|
| 766 |
+
{/if}
|
| 767 |
</div>
|
|
|
|
|
|
|
| 768 |
|
| 769 |
+
<div class="mt-4 grid min-w-0 gap-4 lg:grid-cols-2">
|
| 770 |
+
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 771 |
+
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 772 |
+
<h2 class="font-mono text-sm font-semibold">{m.tools_radar_title()}</h2>
|
| 773 |
+
<span class="text-xs text-muted-foreground">{m.tools_radar_hint()}</span>
|
| 774 |
+
</div>
|
| 775 |
+
{#if selectedModels.length && activeGroups.length >= 3}
|
| 776 |
+
<div class="grid min-w-0 gap-4 lg:grid-cols-[minmax(0,1fr)_24rem] lg:items-start">
|
| 777 |
+
<div class="relative min-w-0 overflow-hidden">
|
| 778 |
+
<svg
|
| 779 |
+
viewBox="0 0 520 420"
|
| 780 |
+
class="h-auto w-full"
|
| 781 |
+
role="img"
|
| 782 |
+
aria-label={m.tools_radar_aria()}
|
| 783 |
+
>
|
| 784 |
+
{#each [0.25, 0.5, 0.75, 1] as scale (scale)}
|
| 785 |
+
<polygon
|
| 786 |
+
points={activeGroups
|
| 787 |
+
.map((_, index) => {
|
| 788 |
+
const point = radarPoint(index, activeGroups.length, 150 * scale);
|
| 789 |
+
return `${point.x.toFixed(1)},${point.y.toFixed(1)}`;
|
| 790 |
+
})
|
| 791 |
+
.join(' ')}
|
| 792 |
+
fill="none"
|
| 793 |
+
stroke="currentColor"
|
| 794 |
+
stroke-width="1"
|
| 795 |
+
stroke-dasharray="3 4"
|
| 796 |
+
pointer-events="none"
|
| 797 |
+
class="text-border"
|
| 798 |
+
/>
|
| 799 |
+
<text
|
| 800 |
+
x="264"
|
| 801 |
+
y={214 - 150 * scale}
|
| 802 |
+
class="fill-muted-foreground font-mono text-[10px]"
|
| 803 |
+
>
|
| 804 |
+
{scale.toFixed(2)}
|
| 805 |
+
</text>
|
| 806 |
+
{/each}
|
| 807 |
+
{#each activeGroups as group, index (group)}
|
| 808 |
+
{@const axis = radarPoint(index, activeGroups.length, 150)}
|
| 809 |
+
<line
|
| 810 |
+
x1="260"
|
| 811 |
+
y1="214"
|
| 812 |
+
x2={axis.x}
|
| 813 |
+
y2={axis.y}
|
| 814 |
+
stroke="currentColor"
|
| 815 |
+
stroke-width="1"
|
| 816 |
+
class="text-border"
|
| 817 |
+
/>
|
| 818 |
+
{/each}
|
| 819 |
+
{#each selectedModels as model (modelKey(model))}
|
| 820 |
+
{@const key = modelKey(model)}
|
| 821 |
+
{@const isPinned = pinnedModelKeys.includes(key)}
|
| 822 |
+
{@const isHovered = hoveredRadarModelKey === key}
|
| 823 |
+
{@const isDimmed = hoveredRadarModelKey !== null && !isHovered}
|
| 824 |
+
<polygon
|
| 825 |
+
points={radarVisualizationPolygon(key, activeGroups, radarScale)}
|
| 826 |
+
fill={colorOf(model)}
|
| 827 |
+
fill-opacity={isHovered
|
| 828 |
+
? 0.24
|
| 829 |
+
: isDimmed
|
| 830 |
+
? 0.025
|
| 831 |
+
: isPinned
|
| 832 |
+
? 0.22
|
| 833 |
+
: pinnedModelKeys.length
|
| 834 |
+
? 0.06
|
| 835 |
+
: 0.12}
|
| 836 |
+
stroke={colorOf(model)}
|
| 837 |
+
stroke-width={isHovered ? 3 : isPinned ? 2.4 : 1.6}
|
| 838 |
+
stroke-opacity={isDimmed ? 0.18 : 1}
|
| 839 |
+
pointer-events="none"
|
| 840 |
+
/>
|
| 841 |
+
{#each activeGroups as group, index (group)}
|
| 842 |
+
{@const value = radarVisualizationValue(key, group, radarScale) ?? 0}
|
| 843 |
+
{@const point = radarPoint(
|
| 844 |
+
index,
|
| 845 |
+
activeGroups.length,
|
| 846 |
+
150 * clamp01(value)
|
| 847 |
+
)}
|
| 848 |
+
<circle
|
| 849 |
+
cx={point.x}
|
| 850 |
+
cy={point.y}
|
| 851 |
+
r={isHovered ? 5 : isPinned ? 4 : 3}
|
| 852 |
+
fill={colorOf(model)}
|
| 853 |
+
fill-opacity={isDimmed ? 0.28 : 1}
|
| 854 |
+
stroke-opacity={isDimmed ? 0.25 : 1}
|
| 855 |
+
class="transition-[r,fill-opacity,stroke-opacity]"
|
| 856 |
+
role="img"
|
| 857 |
+
aria-label={`${modelLabel(model)} ${groupLabel(group)} ${radarScale.toUpperCase()} ${fmt(value, 3)}`}
|
| 858 |
+
onpointerenter={(event) => {
|
| 859 |
+
hoveredRadarModelKey = key;
|
| 860 |
+
tooltip.show(event, modelLabel(model), [
|
| 861 |
+
{ label: groupLabel(group), value: radarScale.toUpperCase() },
|
| 862 |
+
{ label: 'value', value: fmt(value, 3) }
|
| 863 |
+
]);
|
| 864 |
+
}}
|
| 865 |
+
onpointermove={tooltip.move}
|
| 866 |
+
onpointerleave={() => {
|
| 867 |
+
hoveredRadarModelKey = null;
|
| 868 |
+
tooltip.hide();
|
| 869 |
+
}}
|
| 870 |
+
/>
|
| 871 |
+
{/each}
|
| 872 |
+
{/each}
|
| 873 |
+
</svg>
|
| 874 |
+
<RadarLabelsOverlay
|
| 875 |
+
labels={activeGroups.map((group, index) => {
|
| 876 |
+
const label = radarPoint(index, activeGroups.length, 178);
|
| 877 |
+
return {
|
| 878 |
+
key: group,
|
| 879 |
+
text: groupLabel(group),
|
| 880 |
+
x: label.x,
|
| 881 |
+
y: label.y,
|
| 882 |
+
anchor:
|
| 883 |
+
Math.abs(label.x - 260) < 8 ? 'middle' : label.x > 260 ? 'start' : 'end'
|
| 884 |
+
};
|
| 885 |
+
})}
|
| 886 |
+
viewBoxWidth={radarViewBoxWidth}
|
| 887 |
+
viewBoxHeight={radarViewBoxHeight}
|
| 888 |
+
/>
|
| 889 |
+
</div>
|
| 890 |
+
<div
|
| 891 |
+
class="flex min-w-0 flex-wrap content-start gap-x-4 gap-y-2 text-xs text-muted-foreground lg:flex-col lg:gap-x-0"
|
| 892 |
+
>
|
| 893 |
+
{#each selectedModels as model (modelKey(model))}
|
| 894 |
+
{@const key = modelKey(model)}
|
| 895 |
+
{@const isPinned = pinnedModelKeys.includes(key)}
|
| 896 |
+
<span
|
| 897 |
+
class={cn(
|
| 898 |
+
'inline-flex min-w-0 max-w-full items-center gap-2 rounded-full border border-transparent px-2 py-1 transition-colors lg:w-full',
|
| 899 |
+
isPinned &&
|
| 900 |
+
'border-[#0dce97]/35 bg-[#0dce97]/10 text-foreground shadow-sm shadow-[#0dce97]/5'
|
| 901 |
+
)}
|
| 902 |
+
>
|
| 903 |
+
<span class="size-2 rounded-full" style={`background:${colorOf(model)}`}
|
| 904 |
+
></span>
|
| 905 |
+
{#if isPinned}
|
| 906 |
+
<PinIcon class="size-3 text-[#0dce97]" />
|
| 907 |
+
{/if}
|
| 908 |
+
<span class="min-w-0 truncate">{modelLabel(model)}</span>
|
| 909 |
+
<button
|
| 910 |
+
class="ml-auto font-semibold text-[#0dce97]"
|
| 911 |
+
type="button"
|
| 912 |
+
onclick={() => togglePin(key)}
|
| 913 |
+
>
|
| 914 |
+
{isPinned ? m.tools_unpin() : m.tools_pin()}
|
| 915 |
+
</button>
|
| 916 |
+
</span>
|
| 917 |
+
{/each}
|
| 918 |
+
</div>
|
| 919 |
+
</div>
|
| 920 |
+
{:else}
|
| 921 |
+
<p
|
| 922 |
+
class="rounded-lg border bg-muted/30 p-6 text-center text-sm text-muted-foreground"
|
| 923 |
+
>
|
| 924 |
+
{m.tools_radar_empty()}
|
| 925 |
+
</p>
|
| 926 |
+
{/if}
|
| 927 |
+
</section>
|
| 928 |
|
| 929 |
+
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 930 |
+
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 931 |
+
<h2 class="font-mono text-sm font-semibold">FPR × FNR</h2>
|
| 932 |
+
<span class="text-xs text-muted-foreground">{m.tools_scatter_hint()}</span>
|
| 933 |
+
</div>
|
|
|
|
|
|
|
|
|
|
| 934 |
<svg
|
| 935 |
viewBox="0 0 520 420"
|
| 936 |
+
class="mx-auto h-auto w-full max-w-3xl"
|
| 937 |
role="img"
|
| 938 |
+
aria-label={m.tools_scatter_aria()}
|
| 939 |
>
|
| 940 |
+
{#each scatterTicks.slice(1) as tick (tick)}
|
| 941 |
+
<line
|
| 942 |
+
x1={scatterX(tick, scatterXMax)}
|
| 943 |
+
y1="18"
|
| 944 |
+
x2={scatterX(tick, scatterXMax)}
|
| 945 |
+
y2="368"
|
|
|
|
|
|
|
|
|
|
| 946 |
stroke="currentColor"
|
| 947 |
stroke-width="1"
|
| 948 |
stroke-dasharray="3 4"
|
| 949 |
class="text-border"
|
| 950 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
<line
|
| 952 |
+
x1="52"
|
| 953 |
+
y1={scatterY(tick, scatterYMax)}
|
| 954 |
+
x2="502"
|
| 955 |
+
y2={scatterY(tick, scatterYMax)}
|
| 956 |
stroke="currentColor"
|
| 957 |
stroke-width="1"
|
| 958 |
+
stroke-dasharray="3 4"
|
| 959 |
class="text-border"
|
| 960 |
/>
|
| 961 |
+
{/each}
|
| 962 |
+
{#each scatterTicks as tick (tick)}
|
| 963 |
+
<text
|
| 964 |
+
x={scatterX(tick, scatterXMax)}
|
| 965 |
+
y="386"
|
| 966 |
+
text-anchor="middle"
|
| 967 |
+
class="fill-muted-foreground font-mono text-[10px]">{tick.toFixed(2)}</text
|
| 968 |
+
>
|
| 969 |
<text
|
| 970 |
+
x="44"
|
| 971 |
+
y={scatterY(tick, scatterYMax) + 3}
|
| 972 |
+
text-anchor="end"
|
| 973 |
+
class="fill-muted-foreground font-mono text-[10px]">{tick.toFixed(2)}</text
|
|
|
|
|
|
|
|
|
|
|
|
|
| 974 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 975 |
{/each}
|
| 976 |
+
<line x1="52" y1="18" x2="52" y2="368" stroke="currentColor" class="text-border" />
|
| 977 |
+
<line x1="52" y1="368" x2="502" y2="368" stroke="currentColor" class="text-border" />
|
| 978 |
+
<text
|
| 979 |
+
x="277"
|
| 980 |
+
y="408"
|
| 981 |
+
text-anchor="middle"
|
| 982 |
+
class="fill-muted-foreground font-mono text-[10px]">FPR →</text
|
| 983 |
+
>
|
| 984 |
+
<text
|
| 985 |
+
transform="translate(16,193) rotate(-90)"
|
| 986 |
+
text-anchor="middle"
|
| 987 |
+
class="fill-muted-foreground font-mono text-[10px]">FNR →</text
|
| 988 |
+
>
|
| 989 |
+
<text
|
| 990 |
+
x={scatterX(0, scatterXMax)}
|
| 991 |
+
y={scatterY(0, scatterYMax)}
|
| 992 |
+
text-anchor="middle"
|
| 993 |
+
dominant-baseline="central"
|
| 994 |
+
class="fill-[#0dce97] text-sm">★</text
|
| 995 |
+
>
|
| 996 |
+
{#each scatterRows as row (row.modelKey)}
|
| 997 |
+
{@const point = {
|
| 998 |
+
x: scatterX(row.fpr, scatterXMax),
|
| 999 |
+
y: scatterY(row.fnr, scatterYMax)
|
| 1000 |
+
}}
|
| 1001 |
+
{@const isPinned = pinnedModelKeys.includes(row.modelKey)}
|
| 1002 |
+
{@const isHovered = hoveredScatterModelKey === row.modelKey}
|
| 1003 |
+
<circle
|
| 1004 |
+
cx={point.x}
|
| 1005 |
+
cy={point.y}
|
| 1006 |
+
r={isHovered ? 9 : isPinned ? 8 : 6}
|
| 1007 |
+
fill={colorOf(row.model)}
|
| 1008 |
+
fill-opacity="0.85"
|
| 1009 |
+
stroke="currentColor"
|
| 1010 |
+
stroke-width={isPinned ? 2 : 1}
|
| 1011 |
+
class="text-background transition-[r]"
|
| 1012 |
+
role="img"
|
| 1013 |
+
aria-label={`${modelLabel(row.model)} FPR ${fmt(row.fpr, 3)} FNR ${fmt(row.fnr, 3)}`}
|
| 1014 |
+
onpointerenter={(event) => {
|
| 1015 |
+
hoveredScatterModelKey = row.modelKey;
|
| 1016 |
+
tooltip.show(event, modelLabel(row.model), [
|
| 1017 |
+
{ label: 'FPR', value: fmt(row.fpr, 3) },
|
| 1018 |
+
{ label: 'FNR', value: fmt(row.fnr, 3) }
|
| 1019 |
+
]);
|
| 1020 |
+
}}
|
| 1021 |
+
onpointermove={tooltip.move}
|
| 1022 |
+
onpointerleave={() => {
|
| 1023 |
+
hoveredScatterModelKey = null;
|
| 1024 |
+
tooltip.hide();
|
| 1025 |
+
}}
|
| 1026 |
/>
|
| 1027 |
+
{#if isPinned}
|
|
|
|
|
|
|
| 1028 |
<circle
|
| 1029 |
cx={point.x}
|
| 1030 |
cy={point.y}
|
| 1031 |
+
r="11"
|
| 1032 |
+
fill="none"
|
| 1033 |
+
stroke={colorOf(row.model)}
|
| 1034 |
+
stroke-width="1.5"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
/>
|
| 1036 |
+
{/if}
|
| 1037 |
{/each}
|
| 1038 |
</svg>
|
| 1039 |
+
<div class="mt-3 flex flex-wrap gap-4 text-xs text-muted-foreground">
|
| 1040 |
+
<span class="text-[#0dce97]">{m.tools_scatter_ideal()}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1041 |
</div>
|
| 1042 |
+
</section>
|
| 1043 |
+
|
| 1044 |
+
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1045 |
+
<div class="mb-3 flex flex-wrap items-center gap-3">
|
| 1046 |
+
<h2 class="font-mono text-sm font-semibold">
|
| 1047 |
+
Heatmap <span class="font-mono uppercase">{heatmapMetric}</span>
|
| 1048 |
+
</h2>
|
| 1049 |
+
{#if heatmapHasData}
|
| 1050 |
+
<span class="ml-auto inline-flex items-center gap-2 text-xs text-muted-foreground">
|
| 1051 |
+
{m.tools_heatmap_scale_min({ value: heatmapScaleLabel(heatmapColorScale.min) })}
|
| 1052 |
+
<span
|
| 1053 |
+
class="h-2 w-28 rounded-full bg-[linear-gradient(90deg,#0dce97,#ffca28,#ff4d4f)]"
|
| 1054 |
+
></span>
|
| 1055 |
+
{m.tools_heatmap_scale_max({ value: heatmapScaleLabel(heatmapColorScale.max) })}
|
| 1056 |
</span>
|
| 1057 |
+
{/if}
|
| 1058 |
+
</div>
|
| 1059 |
+
{#if heatmapHasData}
|
| 1060 |
+
<div class="max-w-full overflow-auto">
|
| 1061 |
+
<div class="grid min-w-[54rem] gap-1" style:grid-template-columns={heatColumns}>
|
| 1062 |
+
<div
|
| 1063 |
+
class="grid place-items-center px-2 py-2 font-mono text-[10px] text-muted-foreground"
|
| 1064 |
+
>
|
| 1065 |
+
{m.tools_heatmap_header({ metric: heatmapMetric.toUpperCase() })}
|
| 1066 |
+
</div>
|
| 1067 |
+
{#each heatmapModels as model (modelKey(model))}
|
| 1068 |
+
<div
|
| 1069 |
+
class="relative h-24 min-w-0 overflow-visible px-1 pb-2 font-mono text-[10px] text-muted-foreground"
|
| 1070 |
+
>
|
| 1071 |
+
<OverflowCursorTooltip
|
| 1072 |
+
text={modelLabel(model)}
|
| 1073 |
+
labelClass="absolute bottom-2 left-1/2 z-10 block w-28 origin-bottom-left -translate-x-1 rotate-[-40deg] truncate leading-none"
|
| 1074 |
+
/>
|
| 1075 |
+
</div>
|
| 1076 |
+
{/each}
|
| 1077 |
+
{#each heatmapRows as row (row.id)}
|
| 1078 |
+
{@const rowLabel = heatmapRowLabel(row)}
|
| 1079 |
+
<div
|
| 1080 |
+
class="flex min-w-0 items-center justify-end px-2 py-1 text-right text-xs leading-none text-muted-foreground"
|
| 1081 |
+
>
|
| 1082 |
+
<OverflowCursorTooltip text={rowLabel} labelClass="block min-w-0 truncate" />
|
| 1083 |
+
</div>
|
| 1084 |
+
{#each heatmapModels as model (modelKey(model))}
|
| 1085 |
+
{@const key = modelKey(model)}
|
| 1086 |
+
{@const value = heatmapValue(row, key)}
|
| 1087 |
+
{@const hasValue = isNumber(value)}
|
| 1088 |
+
<div
|
| 1089 |
+
role="img"
|
| 1090 |
+
class={cn(
|
| 1091 |
+
'w-full rounded-md px-2 py-2 text-center font-mono text-sm font-semibold',
|
| 1092 |
+
hasValue &&
|
| 1093 |
+
'transition duration-150 hover:brightness-110 hover:saturate-125',
|
| 1094 |
+
!isNumber(value) && 'bg-muted/70 text-muted-foreground',
|
| 1095 |
+
isNumber(value) && heatTextColor(value)
|
| 1096 |
+
)}
|
| 1097 |
+
style={isNumber(value)
|
| 1098 |
+
? `background:${heatColor(
|
| 1099 |
+
value,
|
| 1100 |
+
heatmapColorScale.min ?? value,
|
| 1101 |
+
heatmapColorScale.max ?? value,
|
| 1102 |
+
'sqrt'
|
| 1103 |
+
)}`
|
| 1104 |
+
: ''}
|
| 1105 |
+
aria-label={`${modelLabel(model)} ${rowLabel} ${heatmapMetric.toUpperCase()} ${fmt(value, 3)}`}
|
| 1106 |
+
onpointerenter={(event) =>
|
| 1107 |
+
tooltip.show(event, modelLabel(model), [
|
| 1108 |
+
{ label: groupLabel(row.groupKey), value: rowLabel },
|
| 1109 |
+
{
|
| 1110 |
+
label: heatmapMetric.toUpperCase(),
|
| 1111 |
+
value: fmt(value, 3)
|
| 1112 |
+
}
|
| 1113 |
+
])}
|
| 1114 |
+
onpointermove={tooltip.move}
|
| 1115 |
+
onpointerleave={tooltip.hide}
|
| 1116 |
+
>
|
| 1117 |
+
{isNumber(value) ? fmt(value, 2) : '-'}
|
| 1118 |
+
</div>
|
| 1119 |
+
{/each}
|
| 1120 |
+
{/each}
|
| 1121 |
+
</div>
|
| 1122 |
</div>
|
| 1123 |
+
{:else}
|
|
|
|
| 1124 |
<div
|
| 1125 |
+
class="rounded-lg border border-dashed bg-muted/20 px-4 py-8 text-center text-sm text-muted-foreground"
|
| 1126 |
>
|
| 1127 |
+
{m.tools_heatmap_empty()}
|
| 1128 |
</div>
|
| 1129 |
+
{/if}
|
| 1130 |
+
</section>
|
| 1131 |
+
|
| 1132 |
+
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1133 |
+
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 1134 |
+
<h2 class="font-mono text-sm font-semibold">{m.tools_grouped_bars_title()}</h2>
|
| 1135 |
+
<span class="text-xs text-muted-foreground">{m.tools_grouped_bars_hint()}</span>
|
| 1136 |
+
</div>
|
| 1137 |
+
<div
|
| 1138 |
+
class="max-w-full overflow-x-auto overflow-y-hidden"
|
| 1139 |
+
bind:clientWidth={barContainerWidth}
|
| 1140 |
+
>
|
| 1141 |
+
<div class="relative h-[360px]" style:width={`${barSvgWidth}px`}>
|
| 1142 |
+
<svg
|
| 1143 |
+
viewBox={`0 0 ${barSvgWidth} 360`}
|
| 1144 |
+
class="h-auto max-w-none"
|
| 1145 |
+
style:width={`${barSvgWidth}px`}
|
| 1146 |
role="img"
|
| 1147 |
+
aria-label={m.tools_grouped_bars_aria()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1148 |
>
|
| 1149 |
+
{#each [0, 0.25, 0.5, 0.75, 1] as tick (tick)}
|
| 1150 |
+
<line
|
| 1151 |
+
x1="44"
|
| 1152 |
+
y1={barY(tick)}
|
| 1153 |
+
x2={barSvgWidth - 12}
|
| 1154 |
+
y2={barY(tick)}
|
| 1155 |
+
stroke="currentColor"
|
| 1156 |
+
stroke-width="1"
|
| 1157 |
+
stroke-dasharray="3 4"
|
| 1158 |
+
class="text-border"
|
| 1159 |
+
/>
|
| 1160 |
+
<text
|
| 1161 |
+
x="38"
|
| 1162 |
+
y={barY(tick) + 3}
|
| 1163 |
+
text-anchor="end"
|
| 1164 |
+
class="fill-muted-foreground font-mono text-[13px]">{tick.toFixed(2)}</text
|
| 1165 |
+
>
|
| 1166 |
+
{/each}
|
| 1167 |
+
{#each selectedModels as model, index (modelKey(model))}
|
| 1168 |
+
{@const key = modelKey(model)}
|
| 1169 |
+
{@const band = (barSvgWidth - 56) / selectedModels.length}
|
| 1170 |
+
{@const x0 = 44 + index * band + Math.max(0, (band - barGroupWidth) / 2)}
|
| 1171 |
+
{#each [{ key: 'fnr', color: '#ff4d4f' }, { key: 'fpr', color: '#f5a623' }, { key: 'f1', color: '#0dce97' }] as series, seriesIndex (series.key)}
|
| 1172 |
+
{@const barKey = `${key}-${series.key}`}
|
| 1173 |
+
{@const isHovered = hoveredBarKey === barKey}
|
| 1174 |
+
{@const isDimmed = hoveredBarKey !== null && !isHovered}
|
| 1175 |
+
{@const value = groupedBarsMean(
|
| 1176 |
+
key,
|
| 1177 |
+
series.key as VizMetric,
|
| 1178 |
+
groupedBarsByModelKey,
|
| 1179 |
+
activeGroups
|
| 1180 |
+
)}
|
| 1181 |
+
{#if isNumber(value)}
|
| 1182 |
+
<rect
|
| 1183 |
+
x={x0 + seriesIndex * (barSeriesWidth + barSeriesGap)}
|
| 1184 |
+
y={barY(value)}
|
| 1185 |
+
width={barSeriesWidth}
|
| 1186 |
+
height={268 - barY(value)}
|
| 1187 |
+
rx="2"
|
| 1188 |
+
fill={series.color}
|
| 1189 |
+
fill-opacity={isDimmed ? 0.45 : 1}
|
| 1190 |
+
class={cn(
|
| 1191 |
+
'transition-[fill-opacity,filter]',
|
| 1192 |
+
isHovered && 'brightness-125'
|
| 1193 |
+
)}
|
| 1194 |
+
role="img"
|
| 1195 |
+
aria-label={`${modelLabel(model)} ${series.key.toUpperCase()} ${fmt(value, 3)}`}
|
| 1196 |
+
onpointerenter={(event) => {
|
| 1197 |
+
hoveredBarKey = barKey;
|
| 1198 |
+
tooltip.show(event, modelLabel(model), [
|
| 1199 |
+
{
|
| 1200 |
+
label: series.key.toUpperCase(),
|
| 1201 |
+
value: fmt(value, 3)
|
| 1202 |
+
},
|
| 1203 |
+
{
|
| 1204 |
+
label: m.tools_mean(),
|
| 1205 |
+
value: m.tools_group_mean_summary({ groups: activeGroups.length })
|
| 1206 |
+
}
|
| 1207 |
+
]);
|
| 1208 |
+
}}
|
| 1209 |
+
onpointermove={tooltip.move}
|
| 1210 |
+
onpointerleave={() => {
|
| 1211 |
+
hoveredBarKey = null;
|
| 1212 |
+
tooltip.hide();
|
| 1213 |
+
}}
|
| 1214 |
+
/>
|
| 1215 |
+
{/if}
|
| 1216 |
+
{/each}
|
| 1217 |
+
{/each}
|
| 1218 |
+
</svg>
|
| 1219 |
+
{#each selectedModels as model, index (modelKey(model))}
|
| 1220 |
+
{@const band = (barSvgWidth - 56) / selectedModels.length}
|
| 1221 |
+
{@const x0 = 44 + index * band + Math.max(0, (band - barGroupWidth) / 2)}
|
| 1222 |
+
<div
|
| 1223 |
+
class="absolute h-5 w-20 origin-top-left rotate-45 overflow-visible"
|
| 1224 |
+
style:left={`${x0 + barGroupWidth / 2}px`}
|
| 1225 |
+
style:top="282px"
|
| 1226 |
+
>
|
| 1227 |
+
<OverflowCursorTooltip
|
| 1228 |
+
text={modelLabel(model)}
|
| 1229 |
+
labelClass="block max-w-20 truncate font-mono text-[9px] leading-none text-muted-foreground"
|
| 1230 |
+
tooltipClass="max-w-sm"
|
| 1231 |
+
/>
|
| 1232 |
+
</div>
|
| 1233 |
+
{/each}
|
| 1234 |
+
</div>
|
| 1235 |
+
</div>
|
| 1236 |
+
<div class="mt-3 flex flex-wrap gap-4 text-xs text-muted-foreground">
|
| 1237 |
+
<span class="inline-flex items-center gap-2"
|
| 1238 |
+
><span class="size-2 rounded-full bg-[#ff4d4f]"></span>FNR ({m.tools_lower_better()})</span
|
| 1239 |
+
>
|
| 1240 |
+
<span class="inline-flex items-center gap-2"
|
| 1241 |
+
><span class="size-2 rounded-full bg-[#f5a623]"></span>FPR ({m.tools_lower_better()})</span
|
| 1242 |
+
>
|
| 1243 |
+
<span class="inline-flex items-center gap-2"
|
| 1244 |
+
><span class="size-2 rounded-full bg-[#0dce97]"></span>F1 ({m.tools_higher_better()})</span
|
| 1245 |
+
>
|
| 1246 |
+
</div>
|
| 1247 |
+
</section>
|
| 1248 |
|
| 1249 |
+
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1250 |
+
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 1251 |
+
<h2 class="font-mono text-sm font-semibold">{m.tools_pareto_title()}</h2>
|
| 1252 |
+
<span class="text-xs text-muted-foreground">{m.tools_pareto_hint()}</span>
|
| 1253 |
+
</div>
|
| 1254 |
+
<div class="max-w-full overflow-x-auto">
|
| 1255 |
+
<svg
|
| 1256 |
+
viewBox="0 0 760 380"
|
| 1257 |
+
class="h-auto min-w-[42rem] w-full"
|
| 1258 |
+
role="img"
|
| 1259 |
+
aria-label={m.tools_pareto_aria()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1260 |
>
|
| 1261 |
+
{#each paretoXTicks as xt (xt)}
|
| 1262 |
+
{#if xt > 0}
|
| 1263 |
+
<line
|
| 1264 |
+
x1={paretoX(xt, paretoXMax)}
|
| 1265 |
+
y1="18"
|
| 1266 |
+
x2={paretoX(xt, paretoXMax)}
|
| 1267 |
+
y2="330"
|
| 1268 |
+
stroke="currentColor"
|
| 1269 |
+
stroke-width="1"
|
| 1270 |
+
stroke-dasharray="3 4"
|
| 1271 |
+
class="text-border"
|
| 1272 |
+
/>
|
| 1273 |
+
{/if}
|
| 1274 |
+
<text
|
| 1275 |
+
x={paretoX(xt, paretoXMax)}
|
| 1276 |
+
y="348"
|
| 1277 |
+
text-anchor="middle"
|
| 1278 |
+
class="fill-muted-foreground font-mono text-[7px]">{Math.round(xt)}</text
|
| 1279 |
+
>
|
| 1280 |
+
{/each}
|
| 1281 |
+
{#each paretoYTicks as yt (yt)}
|
| 1282 |
+
<line
|
| 1283 |
+
x1="56"
|
| 1284 |
+
y1={paretoY(yt, paretoY0)}
|
| 1285 |
+
x2="742"
|
| 1286 |
+
y2={paretoY(yt, paretoY0)}
|
| 1287 |
+
stroke="currentColor"
|
| 1288 |
+
stroke-width="1"
|
| 1289 |
+
stroke-dasharray="3 4"
|
| 1290 |
+
class="text-border"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1291 |
/>
|
| 1292 |
+
<text
|
| 1293 |
+
x="48"
|
| 1294 |
+
y={paretoY(yt, paretoY0) + 3}
|
| 1295 |
+
text-anchor="end"
|
| 1296 |
+
class="fill-muted-foreground font-mono text-[7px]">{yt.toFixed(2)}</text
|
| 1297 |
+
>
|
|
|
|
|
|
|
|
|
|
| 1298 |
{/each}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1299 |
<line
|
| 1300 |
+
x1="56"
|
| 1301 |
y1="18"
|
| 1302 |
+
x2="56"
|
| 1303 |
y2="330"
|
| 1304 |
stroke="currentColor"
|
| 1305 |
stroke-width="1"
|
|
|
|
| 1306 |
class="text-border"
|
| 1307 |
/>
|
| 1308 |
+
<line
|
| 1309 |
+
x1="56"
|
| 1310 |
+
y1="330"
|
| 1311 |
+
x2="742"
|
| 1312 |
+
y2="330"
|
| 1313 |
+
stroke="currentColor"
|
| 1314 |
+
stroke-width="1"
|
| 1315 |
+
class="text-border"
|
| 1316 |
+
/>
|
| 1317 |
+
<polyline
|
| 1318 |
+
points={paretoFrontFor(paretoRows, paretoXMax, paretoY0)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1319 |
fill="none"
|
| 1320 |
stroke="#6b4bf5"
|
| 1321 |
+
stroke-width="1.6"
|
| 1322 |
+
stroke-dasharray="5 4"
|
| 1323 |
/>
|
| 1324 |
+
{#each paretoRows as row (row.modelKey)}
|
| 1325 |
+
{@const point = paretoPointFor(row, paretoXMax, paretoY0)}
|
| 1326 |
+
{@const pinned = pinnedModelKeys.includes(row.modelKey)}
|
| 1327 |
+
{@const hovered = hoveredParetoModelKey === row.modelKey}
|
| 1328 |
+
<circle
|
| 1329 |
+
cx={point.x}
|
| 1330 |
+
cy={point.y}
|
| 1331 |
+
r={hovered ? 8 : pinned ? 7 : 5.5}
|
| 1332 |
+
fill={colorOf(row.model)}
|
| 1333 |
+
stroke="currentColor"
|
| 1334 |
+
stroke-width={pinned ? 2 : 1.5}
|
| 1335 |
+
class="cursor-pointer text-background transition-[r] focus:outline-none"
|
| 1336 |
+
role="button"
|
| 1337 |
+
tabindex="0"
|
| 1338 |
+
aria-pressed={pinned}
|
| 1339 |
+
aria-label={pinned
|
| 1340 |
+
? m.tools_pareto_unpin_model({ model: modelLabel(row.model) })
|
| 1341 |
+
: m.tools_pareto_pin_model({ model: modelLabel(row.model) })}
|
| 1342 |
+
onclick={() => togglePin(row.modelKey)}
|
| 1343 |
+
onkeydown={(event) =>
|
| 1344 |
+
handleCheckboxKeydown(event, () => togglePin(row.modelKey))}
|
| 1345 |
+
onfocus={() => (hoveredParetoModelKey = row.modelKey)}
|
| 1346 |
+
onblur={() => (hoveredParetoModelKey = null)}
|
| 1347 |
+
onpointerenter={(event) => {
|
| 1348 |
+
hoveredParetoModelKey = row.modelKey;
|
| 1349 |
+
tooltip.show(event, modelLabel(row.model), [
|
| 1350 |
+
{ label: 'integral', value: fmt(row.integral, 3) },
|
| 1351 |
+
{ label: 'p95', value: `${fmtMs(row.p95)} ${m.ranking_unit_ms()}` }
|
| 1352 |
+
]);
|
| 1353 |
+
}}
|
| 1354 |
+
onpointermove={tooltip.move}
|
| 1355 |
+
onpointerleave={() => {
|
| 1356 |
+
hoveredParetoModelKey = null;
|
| 1357 |
+
tooltip.hide();
|
| 1358 |
+
}}
|
| 1359 |
+
/>
|
| 1360 |
+
{#if pinned}
|
| 1361 |
+
<circle
|
| 1362 |
+
cx={point.x}
|
| 1363 |
+
cy={point.y}
|
| 1364 |
+
r="9.5"
|
| 1365 |
+
fill="none"
|
| 1366 |
+
stroke="#6b4bf5"
|
| 1367 |
+
stroke-width="1.5"
|
| 1368 |
+
pointer-events="none"
|
| 1369 |
+
/>
|
| 1370 |
+
{/if}
|
| 1371 |
+
{/each}
|
| 1372 |
+
{#each paretoRows as row (row.modelKey)}
|
| 1373 |
+
{@const point = paretoPointFor(row, paretoXMax, paretoY0)}
|
| 1374 |
+
{@const pinned = pinnedModelKeys.includes(row.modelKey)}
|
| 1375 |
+
{#if pinned}
|
| 1376 |
+
{@const labelX = Math.min(Math.max(72, point.x), 728)}
|
| 1377 |
+
{@const labelY = Math.max(24, point.y - 18)}
|
| 1378 |
+
<text
|
| 1379 |
+
x={labelX}
|
| 1380 |
+
y={labelY}
|
| 1381 |
+
text-anchor="middle"
|
| 1382 |
+
stroke="var(--card)"
|
| 1383 |
+
stroke-width="4"
|
| 1384 |
+
stroke-linejoin="round"
|
| 1385 |
+
paint-order="stroke"
|
| 1386 |
+
class="fill-muted-foreground font-mono text-[8px]"
|
| 1387 |
+
>
|
| 1388 |
+
{paretoLabelText(row)}
|
| 1389 |
+
</text>
|
| 1390 |
+
{/if}
|
| 1391 |
+
{/each}
|
| 1392 |
<text
|
| 1393 |
+
x="399"
|
| 1394 |
+
y="370"
|
| 1395 |
text-anchor="middle"
|
| 1396 |
+
class="fill-muted-foreground font-mono text-[8px]">{m.tools_pareto_x_axis()}</text
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1397 |
>
|
| 1398 |
+
<text
|
| 1399 |
+
transform="translate(16,174) rotate(-90)"
|
| 1400 |
+
text-anchor="middle"
|
| 1401 |
+
class="fill-muted-foreground font-mono text-[8px]">{m.tools_pareto_y_axis()}</text
|
| 1402 |
+
>
|
| 1403 |
+
</svg>
|
| 1404 |
+
</div>
|
| 1405 |
+
<div class="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
| 1406 |
+
<span>{m.tools_pareto_click_pin_hint()}</span>
|
| 1407 |
+
</div>
|
| 1408 |
+
</section>
|
| 1409 |
+
|
| 1410 |
+
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1411 |
+
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 1412 |
+
<h2 class="font-mono text-sm font-semibold">
|
| 1413 |
+
{m.tools_latency_title()}
|
| 1414 |
+
</h2>
|
| 1415 |
+
<span class="text-xs text-muted-foreground">{m.tools_latency_hint()}</span>
|
| 1416 |
+
</div>
|
| 1417 |
+
<Table
|
| 1418 |
+
data={perfRows}
|
| 1419 |
+
columns={latencyColumns}
|
| 1420 |
+
getRowKey={(row) => row.modelKey}
|
| 1421 |
+
containerClass="max-w-full overflow-x-auto rounded-lg border"
|
| 1422 |
+
tableClass="min-w-[50rem]"
|
| 1423 |
+
headerClass="bg-muted/70 hover:bg-muted/70 [&_th]:text-muted-foreground"
|
| 1424 |
+
rowClass="hover:bg-muted/30"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1425 |
>
|
| 1426 |
+
{#snippet header(column)}
|
| 1427 |
+
{@const isActive = latencySortKey === column.key}
|
| 1428 |
+
<Button
|
| 1429 |
+
variant="ghost"
|
| 1430 |
+
size="sm"
|
| 1431 |
+
class={cn(
|
| 1432 |
+
'h-auto gap-1 px-0 py-0 text-xs font-semibold text-muted-foreground hover:bg-transparent',
|
| 1433 |
+
column.key !== 'model' && 'ml-auto justify-end text-right',
|
| 1434 |
+
isActive && 'text-foreground'
|
| 1435 |
+
)}
|
| 1436 |
+
onclick={() => toggleLatencySort(column.key)}
|
| 1437 |
+
>
|
| 1438 |
+
{latencyColumnLabel(column)}
|
| 1439 |
+
{#if isActive}
|
| 1440 |
+
{#if latencySortDirection === 'asc'}
|
| 1441 |
+
<ArrowUpIcon class="size-3 shrink-0" />
|
| 1442 |
+
{:else}
|
| 1443 |
+
<ArrowDownIcon class="size-3 shrink-0" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1444 |
{/if}
|
| 1445 |
+
{:else}
|
| 1446 |
+
<ArrowUpDownIcon class="size-3 shrink-0 text-muted-foreground" />
|
| 1447 |
+
{/if}
|
| 1448 |
+
</Button>
|
| 1449 |
+
{/snippet}
|
| 1450 |
+
|
| 1451 |
+
{#snippet cell(row, column)}
|
| 1452 |
+
{#if column.key === 'model'}
|
| 1453 |
+
<span
|
| 1454 |
+
class="inline-flex min-w-0 items-center gap-2 font-mono text-xs font-semibold"
|
| 1455 |
+
>
|
| 1456 |
+
<span class="size-2 rounded-full" style={`background:${colorOf(row.model)}`}
|
| 1457 |
+
></span>
|
| 1458 |
+
<span class="min-w-0 max-w-80 truncate">{row.model.short}</span>
|
| 1459 |
+
<code class="text-muted-foreground">{row.model.params}</code>
|
| 1460 |
+
</span>
|
| 1461 |
+
{:else if column.key === 'errorRate'}
|
| 1462 |
+
<span class="font-mono">{fmtPercent(row.errorRate)}</span>
|
| 1463 |
+
{:else}
|
| 1464 |
+
<span class="font-mono">{fmtMs(row[column.key])}</span>
|
| 1465 |
+
{/if}
|
| 1466 |
+
{/snippet}
|
| 1467 |
+
</Table>
|
| 1468 |
+
<p class="mt-3 text-xs text-muted-foreground">{m.tools_latency_note()}</p>
|
| 1469 |
+
</section>
|
| 1470 |
+
|
| 1471 |
+
<section class="min-w-0 rounded-xl border bg-card p-4 shadow-sm lg:col-span-2">
|
| 1472 |
+
<div class="mb-3 flex flex-wrap items-baseline gap-2">
|
| 1473 |
+
<h2 class="font-mono text-sm font-semibold">
|
| 1474 |
+
{m.tools_robustness_title()}
|
| 1475 |
+
</h2>
|
| 1476 |
+
<span class="text-xs text-muted-foreground">{m.tools_robustness_hint()}</span>
|
| 1477 |
+
</div>
|
| 1478 |
+
<p class="mb-3 text-xs leading-5 text-muted-foreground">{m.tools_robustness_note()}</p>
|
| 1479 |
+
<div class="max-w-full overflow-x-auto rounded-lg border">
|
| 1480 |
+
<table class="w-full min-w-[70rem] text-sm">
|
| 1481 |
+
<thead class="bg-muted/70 text-xs text-muted-foreground">
|
| 1482 |
+
<tr>
|
| 1483 |
+
<th class="px-3 py-3 text-left font-semibold">{m.ranking_column_model()}</th>
|
| 1484 |
+
<th class="px-3 py-3 text-right font-semibold">{m.tools_real_score()}</th>
|
| 1485 |
+
<th class="px-3 py-3 text-right font-semibold">{m.tools_robust_score()}</th>
|
| 1486 |
+
<th class="px-3 py-3 text-right font-semibold">Δ score</th>
|
| 1487 |
+
<th class="px-3 py-3 text-right font-semibold">FNR real</th>
|
| 1488 |
+
<th class="px-3 py-3 text-right font-semibold">FNR robust</th>
|
| 1489 |
+
<th class="px-3 py-3 text-right font-semibold">Δ FNR</th>
|
| 1490 |
+
<th class="px-3 py-3 text-right font-semibold">FPR real</th>
|
| 1491 |
+
<th class="px-3 py-3 text-right font-semibold">FPR robust</th>
|
| 1492 |
+
<th class="px-3 py-3 text-right font-semibold">Δ FPR</th>
|
| 1493 |
+
</tr>
|
| 1494 |
+
</thead>
|
| 1495 |
+
<tbody>
|
| 1496 |
+
{#each robustRows as row (row.modelKey)}
|
| 1497 |
+
{@const sDelta = row.deltaScore}
|
| 1498 |
+
{@const fnrDelta = row.deltaFnr}
|
| 1499 |
+
{@const fprDelta = row.deltaFpr}
|
| 1500 |
+
<tr class="border-t transition-colors hover:bg-muted/30">
|
| 1501 |
+
<td class="px-3 py-2">
|
| 1502 |
+
<div class="font-mono text-xs font-semibold">
|
| 1503 |
+
<span
|
| 1504 |
+
class="mr-2 inline-block size-2 rounded-full"
|
| 1505 |
+
style={`background:${colorOf(row.model)}`}
|
| 1506 |
+
></span>
|
| 1507 |
+
<span>{row.model.short}</span>
|
| 1508 |
+
<code class="ml-2 text-muted-foreground">{row.model.params}</code>
|
| 1509 |
+
</div>
|
| 1510 |
+
<div class="mt-2 flex max-w-40 flex-col gap-1">
|
| 1511 |
+
<span class="h-1.5 rounded-full bg-muted">
|
| 1512 |
+
<span
|
| 1513 |
+
class="block h-full rounded-full bg-[#0dce97]"
|
| 1514 |
+
style={`width:${scoreWidth(row.scoreReal)}`}
|
| 1515 |
+
></span>
|
| 1516 |
+
</span>
|
| 1517 |
+
<span class="h-1.5 rounded-full bg-muted">
|
| 1518 |
+
<span
|
| 1519 |
+
class="block h-full rounded-full bg-[#6b4bf5]"
|
| 1520 |
+
style={`width:${scoreWidth(row.scoreRobust)}`}
|
| 1521 |
+
></span>
|
| 1522 |
+
</span>
|
| 1523 |
+
</div>
|
| 1524 |
+
</td>
|
| 1525 |
+
<td class="px-3 py-2 text-right font-mono">{fmt(row.scoreReal, 3)}</td>
|
| 1526 |
+
<td class="px-3 py-2 text-right font-mono">{fmt(row.scoreRobust, 3)}</td>
|
| 1527 |
+
<td class="px-3 py-2 text-right">
|
| 1528 |
<span
|
| 1529 |
+
class={cn(
|
| 1530 |
+
'inline-flex items-center gap-1 rounded-md px-2 py-0.5 font-mono text-xs font-semibold whitespace-nowrap',
|
| 1531 |
+
deltaBadgeClass(sDelta, 'score')
|
| 1532 |
+
)}
|
| 1533 |
+
>
|
| 1534 |
+
{fmtDelta(sDelta)}
|
| 1535 |
+
</span>
|
| 1536 |
+
</td>
|
| 1537 |
+
<td class="px-3 py-2 text-right font-mono">{fmt(row.fnrReal, 3)}</td>
|
| 1538 |
+
<td class="px-3 py-2 text-right font-mono">{fmt(row.fnrRobust, 3)}</td>
|
| 1539 |
+
<td class="px-3 py-2 text-right">
|
| 1540 |
<span
|
| 1541 |
+
class={cn(
|
| 1542 |
+
'inline-flex items-center gap-1 rounded-md px-2 py-0.5 font-mono text-xs font-semibold whitespace-nowrap',
|
| 1543 |
+
deltaBadgeClass(fnrDelta, 'fnr')
|
| 1544 |
+
)}
|
| 1545 |
+
>
|
| 1546 |
+
{fmtDelta(fnrDelta)}
|
| 1547 |
+
</span>
|
| 1548 |
+
</td>
|
| 1549 |
+
<td class="px-3 py-2 text-right font-mono">{fmt(row.fprReal, 3)}</td>
|
| 1550 |
+
<td class="px-3 py-2 text-right font-mono">{fmt(row.fprRobust, 3)}</td>
|
| 1551 |
+
<td class="px-3 py-2 text-right">
|
| 1552 |
+
<span
|
| 1553 |
+
class={cn(
|
| 1554 |
+
'inline-flex items-center gap-1 rounded-md px-2 py-0.5 font-mono text-xs font-semibold whitespace-nowrap',
|
| 1555 |
+
deltaBadgeClass(fprDelta, 'fpr')
|
| 1556 |
+
)}
|
| 1557 |
+
>
|
| 1558 |
+
{fmtDelta(fprDelta)}
|
| 1559 |
+
</span>
|
| 1560 |
+
</td>
|
| 1561 |
+
</tr>
|
| 1562 |
+
{/each}
|
| 1563 |
+
</tbody>
|
| 1564 |
+
</table>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1565 |
</div>
|
| 1566 |
+
</section>
|
| 1567 |
</div>
|
| 1568 |
+
|
| 1569 |
+
<p class="mt-8 text-center text-xs text-muted-foreground">
|
| 1570 |
+
{m.tools_footer()}
|
| 1571 |
+
</p>
|
| 1572 |
</div>
|
| 1573 |
+
{/snippet}
|
| 1574 |
+
</ChartCursorTooltip>
|
| 1575 |
{/if}
|