Spaces:
Running
Running
embed and style improvements
Browse files- app/src/components/Hero.astro +26 -3
- app/src/components/HtmlEmbed.astro +484 -94
- app/src/content/article.mdx +9 -16
- app/src/content/assets/data/stats_L15F21576.csv +3 -0
- app/src/content/assets/data/sweep_1d_metrics.csv +3 -0
- app/src/content/embeds/d3-correlation-matrix.html +466 -0
- app/src/content/embeds/{d3-evaluation1-naive.html → d3-evaluation-configurable.html} +207 -22
- app/src/content/embeds/d3-evaluation-grid.html +0 -467
- app/src/content/embeds/d3-evaluation2-clamp.html +0 -414
- app/src/content/embeds/d3-evaluation3-multi.html +0 -414
- app/src/content/embeds/d3-first-experiments.html +39 -3
- app/src/content/embeds/d3-harmonic-mean.html +842 -0
- app/src/content/embeds/d3-six-line-chart.html +874 -0
- app/src/content/embeds/d3-sweep-1d-metrics.html +862 -0
- app/src/styles/_variables.css +1 -1
app/src/components/Hero.astro
CHANGED
|
@@ -102,9 +102,9 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 102 |
<section class="hero">
|
| 103 |
<h1 class="hero-title" set:html={title} />
|
| 104 |
<div class="hero-banner">
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
{description && <p class="hero-desc">{description}</p>}
|
| 109 |
</div>
|
| 110 |
</section>
|
|
@@ -374,6 +374,29 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
| 374 |
align-items: center;
|
| 375 |
gap: 16px;
|
| 376 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
.hero-desc {
|
| 378 |
color: var(--muted-color);
|
| 379 |
font-style: italic;
|
|
|
|
| 102 |
<section class="hero">
|
| 103 |
<h1 class="hero-title" set:html={title} />
|
| 104 |
<div class="hero-banner">
|
| 105 |
+
<div class="hero-banner-image-wrapper">
|
| 106 |
+
<Image src={eiffel_tower_llama} alt="A llama photobombing the Eiffel Tower" width="700"/>
|
| 107 |
+
</div>
|
| 108 |
{description && <p class="hero-desc">{description}</p>}
|
| 109 |
</div>
|
| 110 |
</section>
|
|
|
|
| 374 |
align-items: center;
|
| 375 |
gap: 16px;
|
| 376 |
}
|
| 377 |
+
.hero-banner-image-wrapper {
|
| 378 |
+
width: 100%;
|
| 379 |
+
max-width: 700px;
|
| 380 |
+
height: 320px;
|
| 381 |
+
border-radius: 16px;
|
| 382 |
+
overflow: hidden;
|
| 383 |
+
display: block;
|
| 384 |
+
}
|
| 385 |
+
.hero-banner-image-wrapper :global(.ri-root),
|
| 386 |
+
.hero-banner-image-wrapper :global(.ri-root img) {
|
| 387 |
+
width: 100%;
|
| 388 |
+
height: 100%;
|
| 389 |
+
object-fit: cover;
|
| 390 |
+
border-radius: 16px;
|
| 391 |
+
display: block;
|
| 392 |
+
}
|
| 393 |
+
.hero-banner-image-wrapper :global(img) {
|
| 394 |
+
width: 100%;
|
| 395 |
+
height: 100%;
|
| 396 |
+
object-fit: cover;
|
| 397 |
+
border-radius: 16px;
|
| 398 |
+
display: block;
|
| 399 |
+
}
|
| 400 |
.hero-desc {
|
| 401 |
color: var(--muted-color);
|
| 402 |
font-style: italic;
|
app/src/components/HtmlEmbed.astro
CHANGED
|
@@ -1,15 +1,44 @@
|
|
| 1 |
---
|
| 2 |
-
interface Props {
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
// Load all .html embeds under src/content/embeds/** as strings (dev & build)
|
| 6 |
-
const embeds = (import.meta as any).glob(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
function resolveFragment(requested: string): string | null {
|
| 9 |
// Allow both "banner.html" and "embeds/banner.html"
|
| 10 |
-
const needle = requested.replace(/^\/*/,
|
| 11 |
for (const [key, html] of Object.entries(embeds)) {
|
| 12 |
-
if (
|
|
|
|
|
|
|
|
|
|
| 13 |
return html;
|
| 14 |
}
|
| 15 |
}
|
|
@@ -18,66 +47,235 @@ function resolveFragment(requested: string): string | null {
|
|
| 18 |
|
| 19 |
const html = resolveFragment(src);
|
| 20 |
const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
| 21 |
-
const dataAttr = Array.isArray(data)
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
// Apply the ID to the HTML content if provided
|
| 25 |
-
const htmlWithId =
|
|
|
|
|
|
|
|
|
|
| 26 |
---
|
| 27 |
-
{ html ? (
|
| 28 |
-
<figure class="html-embed" id={id}>
|
| 29 |
-
{title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
|
| 30 |
-
<div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
|
| 31 |
-
<div id={mountId} data-datafiles={dataAttr} data-config={configAttr} set:html={htmlWithId} />
|
| 32 |
-
</div>
|
| 33 |
-
{desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
|
| 34 |
-
</figure>
|
| 35 |
-
) : (
|
| 36 |
-
<div><!-- Fragment not found: {src} --></div>
|
| 37 |
-
) }
|
| 38 |
-
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
<script>
|
| 42 |
// Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
const
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
if (
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
-
}
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', execute, { once: true });
|
| 69 |
-
else execute();
|
| 70 |
-
</script>
|
| 71 |
|
| 72 |
<style is:global>
|
| 73 |
-
.html-embed {
|
|
|
|
| 74 |
z-index: var(--z-elevated);
|
| 75 |
position: relative;
|
| 76 |
}
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
color: var(--text-color);
|
| 82 |
margin: 0;
|
| 83 |
padding: 0;
|
|
@@ -89,7 +287,7 @@ const htmlWithId = id && html ? html.replace(/<div class="([^"]*)"[^>]*>/, `<div
|
|
| 89 |
z-index: var(--z-elevated);
|
| 90 |
}
|
| 91 |
.html-embed__card {
|
| 92 |
-
background: var(--
|
| 93 |
border: 1px solid var(--border-color);
|
| 94 |
border-radius: 10px;
|
| 95 |
padding: 24px;
|
|
@@ -98,14 +296,14 @@ const htmlWithId = id && html ? html.replace(/<div class="([^"]*)"[^>]*>/, `<div
|
|
| 98 |
}
|
| 99 |
.html-embed__card.is-frameless {
|
| 100 |
background: transparent;
|
| 101 |
-
border
|
| 102 |
padding: 0;
|
| 103 |
}
|
| 104 |
-
.html-embed__desc {
|
| 105 |
-
text-align: left;
|
| 106 |
-
font-size: 0.9rem;
|
| 107 |
-
color: var(--muted-color);
|
| 108 |
-
margin: 0;
|
| 109 |
padding: 0;
|
| 110 |
padding-top: var(--spacing-1);
|
| 111 |
position: relative;
|
|
@@ -114,63 +312,255 @@ const htmlWithId = id && html ? html.replace(/<div class="([^"]*)"[^>]*>/, `<div
|
|
| 114 |
width: 100%;
|
| 115 |
background: var(--page-bg);
|
| 116 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
/* Plotly – fragments & controls */
|
| 118 |
-
.html-embed__card svg text {
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
.
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
/* Dark mode overrides for Plotly readability */
|
| 133 |
-
[data-theme="dark"] .html-embed__card:not(.is-frameless) { background: #12151b; border-color: rgba(255,255,255,.15); }
|
| 134 |
[data-theme="dark"] .html-embed__card .xaxislayer-above text,
|
| 135 |
[data-theme="dark"] .html-embed__card .yaxislayer-above text,
|
| 136 |
[data-theme="dark"] .html-embed__card .infolayer text,
|
| 137 |
[data-theme="dark"] .html-embed__card .legend text,
|
| 138 |
[data-theme="dark"] .html-embed__card .annotation text,
|
| 139 |
[data-theme="dark"] .html-embed__card .colorbar text,
|
| 140 |
-
[data-theme="dark"] .html-embed__card .hoverlayer text {
|
|
|
|
|
|
|
| 141 |
[data-theme="dark"] .html-embed__card .xaxislayer-above path,
|
| 142 |
[data-theme="dark"] .html-embed__card .yaxislayer-above path,
|
| 143 |
[data-theme="dark"] .html-embed__card .xlines-above,
|
| 144 |
-
[data-theme="dark"] .html-embed__card .ylines-above {
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
[data-theme="dark"] .html-embed__card .
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
@media print {
|
| 150 |
-
.html-embed,
|
| 151 |
-
.html-embed__card {
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
.html-embed__card svg,
|
| 154 |
.html-embed__card canvas,
|
| 155 |
-
.html-embed__card img {
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
@media print {
|
| 159 |
/* Avoid breaks inside embeds */
|
| 160 |
-
.html-embed,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
/* Constrain width and scale inner content */
|
| 162 |
-
.html-embed,
|
| 163 |
-
.html-embed__card {
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
.html-embed__card svg,
|
| 166 |
.html-embed__card canvas,
|
| 167 |
.html-embed__card img,
|
| 168 |
.html-embed__card video,
|
| 169 |
-
.html-embed__card iframe {
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
</style>
|
| 175 |
-
|
| 176 |
-
|
|
|
|
| 1 |
---
|
| 2 |
+
interface Props {
|
| 3 |
+
src: string;
|
| 4 |
+
title?: string;
|
| 5 |
+
desc?: string;
|
| 6 |
+
caption?: string;
|
| 7 |
+
frameless?: boolean;
|
| 8 |
+
wide?: boolean;
|
| 9 |
+
align?: "left" | "center" | "right";
|
| 10 |
+
id?: string;
|
| 11 |
+
data?: string | string[];
|
| 12 |
+
config?: any;
|
| 13 |
+
}
|
| 14 |
+
const {
|
| 15 |
+
src,
|
| 16 |
+
title,
|
| 17 |
+
desc,
|
| 18 |
+
caption,
|
| 19 |
+
frameless = false,
|
| 20 |
+
wide = false,
|
| 21 |
+
align = "left",
|
| 22 |
+
id,
|
| 23 |
+
data,
|
| 24 |
+
config,
|
| 25 |
+
} = Astro.props as Props;
|
| 26 |
|
| 27 |
// Load all .html embeds under src/content/embeds/** as strings (dev & build)
|
| 28 |
+
const embeds = (import.meta as any).glob("../content/embeds/**/*.html", {
|
| 29 |
+
query: "?raw",
|
| 30 |
+
import: "default",
|
| 31 |
+
eager: true,
|
| 32 |
+
}) as Record<string, string>;
|
| 33 |
|
| 34 |
function resolveFragment(requested: string): string | null {
|
| 35 |
// Allow both "banner.html" and "embeds/banner.html"
|
| 36 |
+
const needle = requested.replace(/^\/*/, "");
|
| 37 |
for (const [key, html] of Object.entries(embeds)) {
|
| 38 |
+
if (
|
| 39 |
+
key.endsWith("/" + needle) ||
|
| 40 |
+
key.endsWith("/" + needle.replace(/^embeds\//, ""))
|
| 41 |
+
) {
|
| 42 |
return html;
|
| 43 |
}
|
| 44 |
}
|
|
|
|
| 47 |
|
| 48 |
const html = resolveFragment(src);
|
| 49 |
const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
| 50 |
+
const dataAttr = Array.isArray(data)
|
| 51 |
+
? JSON.stringify(data)
|
| 52 |
+
: typeof data === "string"
|
| 53 |
+
? data
|
| 54 |
+
: undefined;
|
| 55 |
+
const configAttr =
|
| 56 |
+
typeof config === "string"
|
| 57 |
+
? config
|
| 58 |
+
: config != null
|
| 59 |
+
? JSON.stringify(config)
|
| 60 |
+
: undefined;
|
| 61 |
|
| 62 |
// Apply the ID to the HTML content if provided
|
| 63 |
+
const htmlWithId =
|
| 64 |
+
id && html
|
| 65 |
+
? html.replace(/<div class="([^"]*)"[^>]*>/, `<div class="$1" id="${id}">`)
|
| 66 |
+
: html;
|
| 67 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
{
|
| 70 |
+
html ? (
|
| 71 |
+
<figure class={`html-embed${wide ? " html-embed--wide" : ""}`} id={id}>
|
| 72 |
+
{title && (
|
| 73 |
+
<figcaption class="html-embed__title" style={`text-align:${align}`}>
|
| 74 |
+
{title}
|
| 75 |
+
</figcaption>
|
| 76 |
+
)}
|
| 77 |
+
<div class={`html-embed__card${frameless ? " is-frameless" : ""}`}>
|
| 78 |
+
<div
|
| 79 |
+
id={mountId}
|
| 80 |
+
data-datafiles={dataAttr}
|
| 81 |
+
data-config={configAttr}
|
| 82 |
+
set:html={htmlWithId}
|
| 83 |
+
/>
|
| 84 |
+
</div>
|
| 85 |
+
{(desc || caption) && (
|
| 86 |
+
<figcaption
|
| 87 |
+
class="html-embed__desc"
|
| 88 |
+
style={`text-align:${align}`}
|
| 89 |
+
set:html={caption || desc}
|
| 90 |
+
/>
|
| 91 |
+
)}
|
| 92 |
+
</figure>
|
| 93 |
+
) : (
|
| 94 |
+
<figure class="html-embed html-embed--error" id={id}>
|
| 95 |
+
<div class="html-embed__card html-embed__card--error">
|
| 96 |
+
<div class="html-embed__error">
|
| 97 |
+
<strong>Embed not found</strong>
|
| 98 |
+
<p>
|
| 99 |
+
The requested embed could not be loaded: <code>{src}</code>
|
| 100 |
+
</p>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</figure>
|
| 104 |
+
)
|
| 105 |
+
}
|
| 106 |
|
| 107 |
<script>
|
| 108 |
// Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
|
| 109 |
+
// Uses IntersectionObserver for lazy loading - only executes when embed is visible
|
| 110 |
+
(() => {
|
| 111 |
+
const scriptEl = document.currentScript;
|
| 112 |
+
const figure = scriptEl?.previousElementSibling?.closest(".html-embed");
|
| 113 |
+
const mount = scriptEl ? scriptEl.previousElementSibling : null;
|
| 114 |
+
|
| 115 |
+
if (!mount || !figure) return;
|
| 116 |
+
|
| 117 |
+
let executed = false;
|
| 118 |
+
const execute = () => {
|
| 119 |
+
if (executed || !mount) return;
|
| 120 |
+
executed = true;
|
| 121 |
+
|
| 122 |
+
const scripts = mount.querySelectorAll("script");
|
| 123 |
+
scripts.forEach((old) => {
|
| 124 |
+
// ignore non-executable types (e.g., application/json)
|
| 125 |
+
if (
|
| 126 |
+
old.type &&
|
| 127 |
+
old.type !== "text/javascript" &&
|
| 128 |
+
old.type !== "module" &&
|
| 129 |
+
old.type !== ""
|
| 130 |
+
)
|
| 131 |
+
return;
|
| 132 |
+
if (old.dataset.executed === "true") return;
|
| 133 |
+
old.dataset.executed = "true";
|
| 134 |
+
if (old.src) {
|
| 135 |
+
const s = document.createElement("script");
|
| 136 |
+
Array.from(old.attributes).forEach((attr) =>
|
| 137 |
+
s.setAttribute(attr.name, attr.value),
|
| 138 |
+
);
|
| 139 |
+
document.body.appendChild(s);
|
| 140 |
+
} else {
|
| 141 |
+
try {
|
| 142 |
+
// run inline
|
| 143 |
+
(0, eval)(old.text || "");
|
| 144 |
+
} catch (e) {
|
| 145 |
+
console.error("HtmlEmbed inline script error:", e);
|
| 146 |
+
}
|
| 147 |
}
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
// Mark as loaded
|
| 151 |
+
figure.classList.add("html-embed--loaded");
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
// Check if IntersectionObserver is supported
|
| 155 |
+
if ("IntersectionObserver" in window) {
|
| 156 |
+
const observer = new IntersectionObserver(
|
| 157 |
+
(entries) => {
|
| 158 |
+
entries.forEach((entry) => {
|
| 159 |
+
if (entry.isIntersecting && !executed) {
|
| 160 |
+
observer.disconnect();
|
| 161 |
+
// Small delay to ensure DOM is ready
|
| 162 |
+
if (document.readyState === "loading") {
|
| 163 |
+
document.addEventListener("DOMContentLoaded", execute, {
|
| 164 |
+
once: true,
|
| 165 |
+
});
|
| 166 |
+
} else {
|
| 167 |
+
// Use requestAnimationFrame to ensure execution after DOM is fully ready
|
| 168 |
+
requestAnimationFrame(() => {
|
| 169 |
+
setTimeout(execute, 0);
|
| 170 |
+
});
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
});
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
// Start loading when element is 100px away from viewport
|
| 177 |
+
rootMargin: "100px",
|
| 178 |
+
threshold: 0.01,
|
| 179 |
+
},
|
| 180 |
+
);
|
| 181 |
+
|
| 182 |
+
observer.observe(figure);
|
| 183 |
+
|
| 184 |
+
// Fallback: if still not loaded after 3 seconds, load anyway (for edge cases)
|
| 185 |
+
setTimeout(() => {
|
| 186 |
+
if (!executed) {
|
| 187 |
+
observer.disconnect();
|
| 188 |
+
if (document.readyState === "loading") {
|
| 189 |
+
document.addEventListener("DOMContentLoaded", execute, {
|
| 190 |
+
once: true,
|
| 191 |
+
});
|
| 192 |
+
} else {
|
| 193 |
+
requestAnimationFrame(() => {
|
| 194 |
+
setTimeout(execute, 0);
|
| 195 |
+
});
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
}, 3000);
|
| 199 |
+
} else {
|
| 200 |
+
// Fallback for browsers without IntersectionObserver support
|
| 201 |
+
if (document.readyState === "loading") {
|
| 202 |
+
document.addEventListener("DOMContentLoaded", execute, { once: true });
|
| 203 |
+
} else {
|
| 204 |
+
requestAnimationFrame(() => {
|
| 205 |
+
setTimeout(execute, 0);
|
| 206 |
+
});
|
| 207 |
}
|
| 208 |
+
}
|
| 209 |
+
})();
|
| 210 |
+
</script>
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
<style is:global>
|
| 213 |
+
.html-embed {
|
| 214 |
+
margin: 0 0 var(--block-spacing-y);
|
| 215 |
z-index: var(--z-elevated);
|
| 216 |
position: relative;
|
| 217 |
}
|
| 218 |
+
|
| 219 |
+
/* Wide mode - same styling as Wide.astro component */
|
| 220 |
+
.html-embed--wide {
|
| 221 |
+
/* Target up to ~1100px while staying within viewport minus page gutters */
|
| 222 |
+
width: min(1100px, 100vw - var(--content-padding-x) * 4);
|
| 223 |
+
margin-left: 50%;
|
| 224 |
+
transform: translateX(-50%);
|
| 225 |
+
padding: calc(var(--content-padding-x) * 4);
|
| 226 |
+
border-radius: calc(var(--button-radius) * 4);
|
| 227 |
+
background-color: var(--page-bg);
|
| 228 |
+
-webkit-mask: linear-gradient(
|
| 229 |
+
to right,
|
| 230 |
+
transparent 0px,
|
| 231 |
+
black 20px,
|
| 232 |
+
black calc(100% - 20px),
|
| 233 |
+
transparent 100%
|
| 234 |
+
),
|
| 235 |
+
linear-gradient(
|
| 236 |
+
to bottom,
|
| 237 |
+
transparent 0px,
|
| 238 |
+
black 20px,
|
| 239 |
+
black calc(100% - 20px),
|
| 240 |
+
transparent 100%
|
| 241 |
+
);
|
| 242 |
+
-webkit-mask-composite: intersect;
|
| 243 |
+
mask: linear-gradient(
|
| 244 |
+
to right,
|
| 245 |
+
transparent 0px,
|
| 246 |
+
black 20px,
|
| 247 |
+
black calc(100% - 20px),
|
| 248 |
+
transparent 100%
|
| 249 |
+
),
|
| 250 |
+
linear-gradient(
|
| 251 |
+
to bottom,
|
| 252 |
+
transparent 0px,
|
| 253 |
+
black 20px,
|
| 254 |
+
black calc(100% - 20px),
|
| 255 |
+
transparent 100%
|
| 256 |
+
);
|
| 257 |
+
mask-composite: intersect;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.html-embed--wide > * {
|
| 261 |
+
margin-bottom: 0 !important;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/* Responsive adjustments for wide mode */
|
| 265 |
+
@media (max-width: 1100px) {
|
| 266 |
+
.html-embed--wide {
|
| 267 |
+
width: 100%;
|
| 268 |
+
margin-left: 0;
|
| 269 |
+
margin-right: 0;
|
| 270 |
+
padding: 0;
|
| 271 |
+
transform: none;
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.html-embed__title {
|
| 276 |
+
text-align: left;
|
| 277 |
+
font-weight: 600;
|
| 278 |
+
font-size: 0.95rem;
|
| 279 |
color: var(--text-color);
|
| 280 |
margin: 0;
|
| 281 |
padding: 0;
|
|
|
|
| 287 |
z-index: var(--z-elevated);
|
| 288 |
}
|
| 289 |
.html-embed__card {
|
| 290 |
+
background-color: var(--surface-bg);
|
| 291 |
border: 1px solid var(--border-color);
|
| 292 |
border-radius: 10px;
|
| 293 |
padding: 24px;
|
|
|
|
| 296 |
}
|
| 297 |
.html-embed__card.is-frameless {
|
| 298 |
background: transparent;
|
| 299 |
+
border: none;
|
| 300 |
padding: 0;
|
| 301 |
}
|
| 302 |
+
.html-embed__desc {
|
| 303 |
+
text-align: left;
|
| 304 |
+
font-size: 0.9rem;
|
| 305 |
+
color: var(--muted-color);
|
| 306 |
+
margin: 0;
|
| 307 |
padding: 0;
|
| 308 |
padding-top: var(--spacing-1);
|
| 309 |
position: relative;
|
|
|
|
| 312 |
width: 100%;
|
| 313 |
background: var(--page-bg);
|
| 314 |
}
|
| 315 |
+
/* Error state for missing embeds */
|
| 316 |
+
.html-embed__card--error {
|
| 317 |
+
background: #fef2f2;
|
| 318 |
+
border: 2px solid #dc2626;
|
| 319 |
+
border-radius: 8px;
|
| 320 |
+
padding: 20px;
|
| 321 |
+
}
|
| 322 |
+
.html-embed__error {
|
| 323 |
+
text-align: center;
|
| 324 |
+
color: #dc2626;
|
| 325 |
+
}
|
| 326 |
+
.html-embed__error strong {
|
| 327 |
+
display: block;
|
| 328 |
+
font-size: 1.1rem;
|
| 329 |
+
font-weight: 600;
|
| 330 |
+
margin-bottom: 8px;
|
| 331 |
+
}
|
| 332 |
+
.html-embed__error p {
|
| 333 |
+
margin: 0;
|
| 334 |
+
font-size: 0.9rem;
|
| 335 |
+
line-height: 1.5;
|
| 336 |
+
}
|
| 337 |
+
.html-embed__error code {
|
| 338 |
+
background: rgba(220, 38, 38, 0.1);
|
| 339 |
+
padding: 2px 6px;
|
| 340 |
+
border-radius: 4px;
|
| 341 |
+
font-family: var(--font-mono);
|
| 342 |
+
font-size: 0.85rem;
|
| 343 |
+
word-break: break-all;
|
| 344 |
+
}
|
| 345 |
+
/* Dark mode for error state */
|
| 346 |
+
[data-theme="dark"] .html-embed__card--error {
|
| 347 |
+
background: #1f2937;
|
| 348 |
+
border-color: #ef4444;
|
| 349 |
+
}
|
| 350 |
+
[data-theme="dark"] .html-embed__error {
|
| 351 |
+
color: #ef4444;
|
| 352 |
+
}
|
| 353 |
+
[data-theme="dark"] .html-embed__error code {
|
| 354 |
+
background: rgba(239, 68, 68, 0.2);
|
| 355 |
+
}
|
| 356 |
/* Plotly – fragments & controls */
|
| 357 |
+
.html-embed__card svg text {
|
| 358 |
+
fill: var(--text-color);
|
| 359 |
+
}
|
| 360 |
+
.html-embed__card label {
|
| 361 |
+
color: var(--text-color);
|
| 362 |
+
}
|
| 363 |
+
.plotly-graph-div {
|
| 364 |
+
width: 100%;
|
| 365 |
+
min-height: 320px;
|
| 366 |
+
}
|
| 367 |
+
@media (max-width: 768px) {
|
| 368 |
+
.plotly-graph-div {
|
| 369 |
+
min-height: 260px;
|
| 370 |
+
}
|
| 371 |
+
}
|
| 372 |
+
[id^="plot-"] {
|
| 373 |
+
display: flex;
|
| 374 |
+
flex-direction: column;
|
| 375 |
+
align-items: center;
|
| 376 |
+
gap: 15px;
|
| 377 |
+
}
|
| 378 |
+
.plotly_caption {
|
| 379 |
+
font-style: italic;
|
| 380 |
+
margin-top: 10px;
|
| 381 |
+
}
|
| 382 |
+
.plotly_controls {
|
| 383 |
+
display: flex;
|
| 384 |
+
flex-wrap: wrap;
|
| 385 |
+
justify-content: center;
|
| 386 |
+
gap: 30px;
|
| 387 |
+
}
|
| 388 |
+
.plotly_input_container {
|
| 389 |
+
display: flex;
|
| 390 |
+
align-items: center;
|
| 391 |
+
flex-direction: column;
|
| 392 |
+
gap: 10px;
|
| 393 |
+
}
|
| 394 |
+
.plotly_input_container > select {
|
| 395 |
+
padding: 2px 4px;
|
| 396 |
+
line-height: 1.5em;
|
| 397 |
+
text-align: center;
|
| 398 |
+
border-radius: 4px;
|
| 399 |
+
font-size: 12px;
|
| 400 |
+
background-color: var(--neutral-200);
|
| 401 |
+
outline: none;
|
| 402 |
+
border: 1px solid var(--neutral-300);
|
| 403 |
+
}
|
| 404 |
+
.plotly_slider {
|
| 405 |
+
display: flex;
|
| 406 |
+
align-items: center;
|
| 407 |
+
gap: 10px;
|
| 408 |
+
}
|
| 409 |
+
.plotly_slider > input[type="range"] {
|
| 410 |
+
-webkit-appearance: none;
|
| 411 |
+
appearance: none;
|
| 412 |
+
height: 2px;
|
| 413 |
+
background: var(--neutral-400);
|
| 414 |
+
border-radius: 5px;
|
| 415 |
+
outline: none;
|
| 416 |
+
}
|
| 417 |
+
.plotly_slider > input[type="range"]::-webkit-slider-thumb {
|
| 418 |
+
-webkit-appearance: none;
|
| 419 |
+
width: 18px;
|
| 420 |
+
height: 18px;
|
| 421 |
+
border-radius: 50%;
|
| 422 |
+
background: var(--primary-color);
|
| 423 |
+
cursor: pointer;
|
| 424 |
+
}
|
| 425 |
+
.plotly_slider > input[type="range"]::-moz-range-thumb {
|
| 426 |
+
width: 18px;
|
| 427 |
+
height: 18px;
|
| 428 |
+
border-radius: 50%;
|
| 429 |
+
background: var(--primary-color);
|
| 430 |
+
cursor: pointer;
|
| 431 |
+
}
|
| 432 |
+
.plotly_slider > span {
|
| 433 |
+
font-size: 14px;
|
| 434 |
+
line-height: 1.6em;
|
| 435 |
+
min-width: 16px;
|
| 436 |
+
}
|
| 437 |
/* Dark mode overrides for Plotly readability */
|
|
|
|
| 438 |
[data-theme="dark"] .html-embed__card .xaxislayer-above text,
|
| 439 |
[data-theme="dark"] .html-embed__card .yaxislayer-above text,
|
| 440 |
[data-theme="dark"] .html-embed__card .infolayer text,
|
| 441 |
[data-theme="dark"] .html-embed__card .legend text,
|
| 442 |
[data-theme="dark"] .html-embed__card .annotation text,
|
| 443 |
[data-theme="dark"] .html-embed__card .colorbar text,
|
| 444 |
+
[data-theme="dark"] .html-embed__card .hoverlayer text {
|
| 445 |
+
fill: #fff !important;
|
| 446 |
+
}
|
| 447 |
[data-theme="dark"] .html-embed__card .xaxislayer-above path,
|
| 448 |
[data-theme="dark"] .html-embed__card .yaxislayer-above path,
|
| 449 |
[data-theme="dark"] .html-embed__card .xlines-above,
|
| 450 |
+
[data-theme="dark"] .html-embed__card .ylines-above {
|
| 451 |
+
stroke: rgba(255, 255, 255, 0.35) !important;
|
| 452 |
+
}
|
| 453 |
+
[data-theme="dark"] .html-embed__card .gridlayer path {
|
| 454 |
+
stroke: rgba(255, 255, 255, 0.15) !important;
|
| 455 |
+
}
|
| 456 |
+
[data-theme="dark"] .html-embed__card .legend rect.bg {
|
| 457 |
+
fill: rgba(0, 0, 0, 0.25) !important;
|
| 458 |
+
stroke: rgba(255, 255, 255, 0.2) !important;
|
| 459 |
+
}
|
| 460 |
+
[data-theme="dark"] .html-embed__card .hoverlayer .bg {
|
| 461 |
+
fill: rgba(0, 0, 0, 0.8) !important;
|
| 462 |
+
stroke: rgba(255, 255, 255, 0.2) !important;
|
| 463 |
+
}
|
| 464 |
+
[data-theme="dark"] .html-embed__card .colorbar .cbbg {
|
| 465 |
+
fill: rgba(0, 0, 0, 0.25) !important;
|
| 466 |
+
stroke: rgba(255, 255, 255, 0.2) !important;
|
| 467 |
+
}
|
| 468 |
@media print {
|
| 469 |
+
.html-embed,
|
| 470 |
+
.html-embed__card {
|
| 471 |
+
max-width: 100% !important;
|
| 472 |
+
width: 100% !important;
|
| 473 |
+
margin-left: 0 !important;
|
| 474 |
+
margin-right: 0 !important;
|
| 475 |
+
}
|
| 476 |
+
.html-embed__card {
|
| 477 |
+
padding: 6px;
|
| 478 |
+
}
|
| 479 |
+
.html-embed__card.is-frameless {
|
| 480 |
+
padding: 0;
|
| 481 |
+
}
|
| 482 |
.html-embed__card svg,
|
| 483 |
.html-embed__card canvas,
|
| 484 |
+
.html-embed__card img {
|
| 485 |
+
max-width: 100% !important;
|
| 486 |
+
height: auto !important;
|
| 487 |
+
}
|
| 488 |
+
.html-embed__card > div[id^="frag-"] {
|
| 489 |
+
width: 100% !important;
|
| 490 |
+
}
|
| 491 |
}
|
| 492 |
@media print {
|
| 493 |
/* Avoid breaks inside embeds */
|
| 494 |
+
.html-embed,
|
| 495 |
+
.html-embed__card {
|
| 496 |
+
break-inside: avoid;
|
| 497 |
+
page-break-inside: avoid;
|
| 498 |
+
}
|
| 499 |
/* Constrain width and scale inner content */
|
| 500 |
+
.html-embed,
|
| 501 |
+
.html-embed__card {
|
| 502 |
+
max-width: 100% !important;
|
| 503 |
+
width: 100% !important;
|
| 504 |
+
}
|
| 505 |
+
.html-embed__card {
|
| 506 |
+
padding: 6px;
|
| 507 |
+
}
|
| 508 |
+
.html-embed__card.is-frameless {
|
| 509 |
+
padding: 0;
|
| 510 |
+
}
|
| 511 |
.html-embed__card svg,
|
| 512 |
.html-embed__card canvas,
|
| 513 |
.html-embed__card img,
|
| 514 |
.html-embed__card video,
|
| 515 |
+
.html-embed__card iframe {
|
| 516 |
+
max-width: 100% !important;
|
| 517 |
+
height: auto !important;
|
| 518 |
+
}
|
| 519 |
+
.html-embed__card > div[id^="frag-"] {
|
| 520 |
+
width: 100% !important;
|
| 521 |
+
max-width: 100% !important;
|
| 522 |
+
}
|
| 523 |
+
/* Center and constrain all banners when printing */
|
| 524 |
+
.html-embed .d3-galaxy,
|
| 525 |
+
.html-embed .threejs-galaxy,
|
| 526 |
+
.html-embed .d3-latent-space,
|
| 527 |
+
.html-embed .neural-flow,
|
| 528 |
+
.html-embed .molecular-space,
|
| 529 |
+
.html-embed [class*="banner"] {
|
| 530 |
+
width: 100% !important;
|
| 531 |
+
max-width: 980px !important;
|
| 532 |
+
margin-left: auto !important;
|
| 533 |
+
margin-right: auto !important;
|
| 534 |
+
}
|
| 535 |
+
/* Better rendering for d3-loss-curves when printing */
|
| 536 |
+
.html-embed .d3-loss-curves {
|
| 537 |
+
width: 100% !important;
|
| 538 |
+
height: auto !important;
|
| 539 |
+
min-height: 300px !important;
|
| 540 |
+
margin-left: auto !important;
|
| 541 |
+
margin-right: auto !important;
|
| 542 |
+
overflow: visible !important;
|
| 543 |
+
}
|
| 544 |
+
.html-embed .d3-loss-curves svg {
|
| 545 |
+
width: 100% !important;
|
| 546 |
+
height: auto !important;
|
| 547 |
+
max-height: 500px !important;
|
| 548 |
+
}
|
| 549 |
+
/* Ensure legend is visible in print */
|
| 550 |
+
.html-embed .d3-loss-curves .legend {
|
| 551 |
+
position: relative !important;
|
| 552 |
+
display: flex !important;
|
| 553 |
+
flex-direction: column !important;
|
| 554 |
+
align-items: flex-start !important;
|
| 555 |
+
gap: 4px !important;
|
| 556 |
+
margin-top: 10px !important;
|
| 557 |
+
bottom: auto !important;
|
| 558 |
+
left: auto !important;
|
| 559 |
+
max-width: 100% !important;
|
| 560 |
+
}
|
| 561 |
+
/* Hide annotation in print */
|
| 562 |
+
.html-embed .d3-loss-curves .annotation {
|
| 563 |
+
display: none !important;
|
| 564 |
+
}
|
| 565 |
}
|
| 566 |
</style>
|
|
|
|
|
|
app/src/content/article.mdx
CHANGED
|
@@ -11,7 +11,6 @@ affiliations:
|
|
| 11 |
- name: "Hugging Face"
|
| 12 |
url: "https://huggingface.co"
|
| 13 |
published: "Nov. 18, 2025"
|
| 14 |
-
doi: 10.1234/abcd.efgh
|
| 15 |
licence: >
|
| 16 |
Diagrams and text are licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">CC‑BY 4.0</a> with the source available on <a href="https://huggingface.co/spaces/tfrere/research-article-template" target="_blank" rel="noopener noreferrer">Hugging Face</a>, unless noted otherwise.
|
| 17 |
Figures reused from other sources are excluded and marked in their captions (“Figure from …”).
|
|
@@ -19,7 +18,7 @@ tags:
|
|
| 19 |
- research
|
| 20 |
- template
|
| 21 |
tableOfContentsAutoCollapse: true
|
| 22 |
-
pdfProOnly:
|
| 23 |
---
|
| 24 |
|
| 25 |
import Image from '../components/Image.astro'
|
|
@@ -289,9 +288,9 @@ import activations_magnitude from './assets/image/activations_magnitude.png'
|
|
| 289 |
|
| 290 |
As we can see, activation norms roughly grow linearly across layers, with a norm being approximately equal to the layer index.
|
| 291 |
If we want to look for a steering coefficient that is typically less than the original activation vector norm at layer $l$,
|
| 292 |
-
we can define a reduced coefficient $\hat{\
|
| 293 |
$$
|
| 294 |
-
\hat{\
|
| 295 |
$$
|
| 296 |
|
| 297 |
|
|
@@ -302,9 +301,7 @@ For a first grid search, we used the set of 50 prompts, temperature was set to 1
|
|
| 302 |
The image below shows the results for each of our six metrics of the sweep over $\alpha$ for the feature 21576 in layer 15.
|
| 303 |
The left column displays the three LLM-judge metrics, while the right column shows our three auxiliary metrics. On those charts, we can observe several regimes corresponding to essentially three ranges of the steering coefficient.
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
<Image src={sweep_1D_analysis} alt="1D sweep of steering coefficient" caption="1D sweep of steering coefficient for a single steering vector, with six metrics monitored." />
|
| 308 |
|
| 309 |
First of all, **for low values of the steering coefficient $\alpha < 5$, the steered model behaves almost as the reference model**:
|
| 310 |
the concept inclusion metric is zero, instruction following and fluency are close to 2.0, equivalent to the reference model.
|
|
@@ -324,9 +321,7 @@ Inspection of the answers shows that the model is producing repetitive patterns
|
|
| 324 |
|
| 325 |
Those metrics show that we face a fundamental trade-off: stronger steering increases concept inclusion but degrades fluency, and finding the balance is the challenge. This is further complicated by the very large standard deviation: for a given steering coefficient, some prompts lead to good results while others completely fail. Even though all metrics somehow tell the same story, we have to decide how to select the optimal steering coefficient. We could simply use the mean of the three LLM judge metrics, but we can easily see that this would lead us to select the unsteered model (low $\alpha$) as the best model, which is not what we want. For that, we can use **the harmonic mean criterion proposed by AxBench**.
|
| 326 |
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
<Image src={harmonic_mean_curve} alt="Arithmetic (left) and harmonic (right) mean of the three LLM-judge metrics as a function of steering coefficient." caption="Arithmetic (left) and harmonic (right) mean of the three LLM-judge metrics as a function of steering coefficient." />
|
| 330 |
|
| 331 |
First, the results show the harmonic mean curve is very noisy. Despite the fact that we used 50 prompts to evaluate each point, the inherent discreteness of the LLM-judge metrics and the stochasticity of LLM generation leads to a noisy harmonic mean. This is something to keep in mind when trying to optimize steering coefficients.
|
| 332 |
|
|
@@ -345,7 +340,7 @@ Note that the harmonic mean we obtained here (about 0.45) is higher than the one
|
|
| 345 |
|
| 346 |
Using the optimal steering coefficient $\alpha=8.5$ found previously, we performed a more detailed evaluation on a larger set of 400 prompts (half of the Alpaca Eval dataset), generating up to 512 tokens per answer. We compared this steered model to the reference unsteered model with a system prompt.
|
| 347 |
|
| 348 |
-
<HtmlEmbed src="d3-
|
| 349 |
|
| 350 |
We can see that on all metrics, **the baseline prompted model significantly outperforms the steered model.** This is consistent with the findings by AxBench that steering with SAEs is not very effective. However, our numbers are not as dire as theirs. We can see an average score in concept inclusion compared to the reference model (1.03), while maintaining a reasonable level of instruction following (1.35). However, this comes at the price of a fluency drop (0.78 vs. 1.55 for the prompted model), as fluency is impaired by repetitions (0.27) or awkward phrasing.
|
| 351 |
|
|
@@ -363,9 +358,7 @@ Overall, the harmonic mean of the three LLM-judge metrics is 1.67 for the prompt
|
|
| 363 |
|
| 364 |
From the results of this sweep, we can compute the correlations between our six metrics to see how they relate to each other.
|
| 365 |
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
<Image src={metrics_correlation} alt="Correlation matrix between metrics" caption="Correlation matrix between metrics." />
|
| 369 |
|
| 370 |
The matrix above shows several interesting correlations.
|
| 371 |
First, **LLM instruction following and fluency are highly correlated** (0.8), which is not surprising as both metrics
|
|
@@ -399,7 +392,7 @@ This clamping approach was the one used by Anthropic in their Golden Gate demo,
|
|
| 399 |
|
| 400 |
We tested the impact of clamping on the same steering vector at the optimal steering coefficient found previously ($\alpha=8.5$). We evaluated the model on the same set of prompts with 20 samples each and a maximum output length of 512 tokens.
|
| 401 |
|
| 402 |
-
<HtmlEmbed src="d3-
|
| 403 |
|
| 404 |
We can see that **clamping has a positive effect on concept inclusion (both from the LLM score and the explicit reference), while not harming the other metrics**.
|
| 405 |
|
|
@@ -483,7 +476,7 @@ We performed optimization using 2 features (from layer 15 and layer 19) and then
|
|
| 483 |
|
| 484 |
Results are shown below and compared to single-layer steering.
|
| 485 |
|
| 486 |
-
<HtmlEmbed src="d3-
|
| 487 |
|
| 488 |
As we can see on the chart, steering 2 or even 8 features simultaneously leads to **only marginal improvements** compared to steering only one feature. Although fluency and instruction following are improved, concept inclusion slightly decreases, leading to a harmonic mean that is only marginally better than single-layer steering.
|
| 489 |
|
|
|
|
| 11 |
- name: "Hugging Face"
|
| 12 |
url: "https://huggingface.co"
|
| 13 |
published: "Nov. 18, 2025"
|
|
|
|
| 14 |
licence: >
|
| 15 |
Diagrams and text are licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">CC‑BY 4.0</a> with the source available on <a href="https://huggingface.co/spaces/tfrere/research-article-template" target="_blank" rel="noopener noreferrer">Hugging Face</a>, unless noted otherwise.
|
| 16 |
Figures reused from other sources are excluded and marked in their captions (“Figure from …”).
|
|
|
|
| 18 |
- research
|
| 19 |
- template
|
| 20 |
tableOfContentsAutoCollapse: true
|
| 21 |
+
pdfProOnly: true
|
| 22 |
---
|
| 23 |
|
| 24 |
import Image from '../components/Image.astro'
|
|
|
|
| 288 |
|
| 289 |
As we can see, activation norms roughly grow linearly across layers, with a norm being approximately equal to the layer index.
|
| 290 |
If we want to look for a steering coefficient that is typically less than the original activation vector norm at layer $l$,
|
| 291 |
+
we can define a reduced coefficient $\hat{\alpha}_l = (\alpha_l / l)$, and restrict our search to
|
| 292 |
$$
|
| 293 |
+
\hat{\alpha}_l \in [0,1]
|
| 294 |
$$
|
| 295 |
|
| 296 |
|
|
|
|
| 301 |
The image below shows the results for each of our six metrics of the sweep over $\alpha$ for the feature 21576 in layer 15.
|
| 302 |
The left column displays the three LLM-judge metrics, while the right column shows our three auxiliary metrics. On those charts, we can observe several regimes corresponding to essentially three ranges of the steering coefficient.
|
| 303 |
|
| 304 |
+
<HtmlEmbed src="d3-sweep-1d-metrics.html" data="stats_L15F21576.csv" />
|
|
|
|
|
|
|
| 305 |
|
| 306 |
First of all, **for low values of the steering coefficient $\alpha < 5$, the steered model behaves almost as the reference model**:
|
| 307 |
the concept inclusion metric is zero, instruction following and fluency are close to 2.0, equivalent to the reference model.
|
|
|
|
| 321 |
|
| 322 |
Those metrics show that we face a fundamental trade-off: stronger steering increases concept inclusion but degrades fluency, and finding the balance is the challenge. This is further complicated by the very large standard deviation: for a given steering coefficient, some prompts lead to good results while others completely fail. Even though all metrics somehow tell the same story, we have to decide how to select the optimal steering coefficient. We could simply use the mean of the three LLM judge metrics, but we can easily see that this would lead us to select the unsteered model (low $\alpha$) as the best model, which is not what we want. For that, we can use **the harmonic mean criterion proposed by AxBench**.
|
| 323 |
|
| 324 |
+
<HtmlEmbed src="d3-harmonic-mean.html" data="stats_L15F21576.csv" />
|
|
|
|
|
|
|
| 325 |
|
| 326 |
First, the results show the harmonic mean curve is very noisy. Despite the fact that we used 50 prompts to evaluate each point, the inherent discreteness of the LLM-judge metrics and the stochasticity of LLM generation leads to a noisy harmonic mean. This is something to keep in mind when trying to optimize steering coefficients.
|
| 327 |
|
|
|
|
| 340 |
|
| 341 |
Using the optimal steering coefficient $\alpha=8.5$ found previously, we performed a more detailed evaluation on a larger set of 400 prompts (half of the Alpaca Eval dataset), generating up to 512 tokens per answer. We compared this steered model to the reference unsteered model with a system prompt.
|
| 342 |
|
| 343 |
+
<HtmlEmbed src="d3-evaluation-configurable.html" data="evaluation_summary.json" config="naive" />
|
| 344 |
|
| 345 |
We can see that on all metrics, **the baseline prompted model significantly outperforms the steered model.** This is consistent with the findings by AxBench that steering with SAEs is not very effective. However, our numbers are not as dire as theirs. We can see an average score in concept inclusion compared to the reference model (1.03), while maintaining a reasonable level of instruction following (1.35). However, this comes at the price of a fluency drop (0.78 vs. 1.55 for the prompted model), as fluency is impaired by repetitions (0.27) or awkward phrasing.
|
| 346 |
|
|
|
|
| 358 |
|
| 359 |
From the results of this sweep, we can compute the correlations between our six metrics to see how they relate to each other.
|
| 360 |
|
| 361 |
+
<HtmlEmbed src="d3-correlation-matrix.html" caption="Correlation matrix between metrics." />
|
|
|
|
|
|
|
| 362 |
|
| 363 |
The matrix above shows several interesting correlations.
|
| 364 |
First, **LLM instruction following and fluency are highly correlated** (0.8), which is not surprising as both metrics
|
|
|
|
| 392 |
|
| 393 |
We tested the impact of clamping on the same steering vector at the optimal steering coefficient found previously ($\alpha=8.5$). We evaluated the model on the same set of prompts with 20 samples each and a maximum output length of 512 tokens.
|
| 394 |
|
| 395 |
+
<HtmlEmbed src="d3-evaluation-configurable.html" data="evaluation_summary.json" config="clamp" />
|
| 396 |
|
| 397 |
We can see that **clamping has a positive effect on concept inclusion (both from the LLM score and the explicit reference), while not harming the other metrics**.
|
| 398 |
|
|
|
|
| 476 |
|
| 477 |
Results are shown below and compared to single-layer steering.
|
| 478 |
|
| 479 |
+
<HtmlEmbed src="d3-evaluation-configurable.html" data="evaluation_summary.json" config="multi" />
|
| 480 |
|
| 481 |
As we can see on the chart, steering 2 or even 8 features simultaneously leads to **only marginal improvements** compared to steering only one feature. Although fluency and instruction following are improved, concept inclusion slightly decreases, leading to a harmonic mean that is only marginally better than single-layer steering.
|
| 482 |
|
app/src/content/assets/data/stats_L15F21576.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b9d6e8bec82fc1be821a6a36dc673591b87a9542d058e2c149477e5bfb6e2fa7
|
| 3 |
+
size 40437
|
app/src/content/assets/data/sweep_1d_metrics.csv
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7cfd8216d57a41ab1fa38f787cc245955a73057d4d4cdc793e3a73c5274f2399
|
| 3 |
+
size 1529
|
app/src/content/embeds/d3-correlation-matrix.html
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="d3-correlation-matrix"></div>
|
| 2 |
+
<style>
|
| 3 |
+
.d3-correlation-matrix {
|
| 4 |
+
position: relative;
|
| 5 |
+
overflow: visible;
|
| 6 |
+
}
|
| 7 |
+
.d3-correlation-matrix .chart-card {
|
| 8 |
+
background: var(--surface-bg);
|
| 9 |
+
border: none;
|
| 10 |
+
border-radius: 10px;
|
| 11 |
+
padding: 0;
|
| 12 |
+
overflow: visible;
|
| 13 |
+
}
|
| 14 |
+
.d3-correlation-matrix .chart-card svg {
|
| 15 |
+
overflow: visible;
|
| 16 |
+
}
|
| 17 |
+
.d3-correlation-matrix .axis-label-x {
|
| 18 |
+
fill: var(--text-color);
|
| 19 |
+
font-size: 11px;
|
| 20 |
+
font-weight: 700;
|
| 21 |
+
}
|
| 22 |
+
.d3-correlation-matrix .axis-label-x text {
|
| 23 |
+
word-break: break-word;
|
| 24 |
+
white-space: normal;
|
| 25 |
+
}
|
| 26 |
+
.d3-correlation-matrix .axis-label {
|
| 27 |
+
fill: var(--text-color);
|
| 28 |
+
font-size: 11px;
|
| 29 |
+
font-weight: 700;
|
| 30 |
+
}
|
| 31 |
+
.d3-correlation-matrix .cell-text {
|
| 32 |
+
fill: var(--muted-color);
|
| 33 |
+
font-size: 11px;
|
| 34 |
+
pointer-events: none;
|
| 35 |
+
}
|
| 36 |
+
.d3-correlation-matrix .colorbar-label {
|
| 37 |
+
fill: var(--text-color);
|
| 38 |
+
font-size: 10px;
|
| 39 |
+
font-weight: 600;
|
| 40 |
+
}
|
| 41 |
+
</style>
|
| 42 |
+
<script>
|
| 43 |
+
(() => {
|
| 44 |
+
// Load D3 from CDN once
|
| 45 |
+
const ensureD3 = (cb) => {
|
| 46 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 47 |
+
let s = document.getElementById('d3-cdn-script');
|
| 48 |
+
if (!s) {
|
| 49 |
+
s = document.createElement('script');
|
| 50 |
+
s.id = 'd3-cdn-script';
|
| 51 |
+
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 52 |
+
document.head.appendChild(s);
|
| 53 |
+
}
|
| 54 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 55 |
+
s.addEventListener('load', onReady, { once: true });
|
| 56 |
+
if (window.d3) onReady();
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
const bootstrap = () => {
|
| 60 |
+
const scriptEl = document.currentScript;
|
| 61 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 62 |
+
if (!(container && container.classList && container.classList.contains('d3-correlation-matrix'))){
|
| 63 |
+
const cs = Array.from(document.querySelectorAll('.d3-correlation-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 64 |
+
container = cs[cs.length - 1] || null;
|
| 65 |
+
}
|
| 66 |
+
if (!container) return;
|
| 67 |
+
if (container.dataset) {
|
| 68 |
+
if (container.dataset.mounted === 'true') return;
|
| 69 |
+
container.dataset.mounted = 'true';
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Tooltip (HTML, single instance inside container)
|
| 73 |
+
container.style.position = container.style.position || 'relative';
|
| 74 |
+
let tip = container.querySelector('.d3-tooltip');
|
| 75 |
+
let tipInner;
|
| 76 |
+
if (!tip) {
|
| 77 |
+
tip = document.createElement('div');
|
| 78 |
+
tip.className = 'd3-tooltip';
|
| 79 |
+
Object.assign(tip.style, {
|
| 80 |
+
position: 'absolute',
|
| 81 |
+
top: '0px',
|
| 82 |
+
left: '0px',
|
| 83 |
+
transform: 'translate(-9999px, -9999px)',
|
| 84 |
+
pointerEvents: 'none',
|
| 85 |
+
padding: '8px 10px',
|
| 86 |
+
borderRadius: '8px',
|
| 87 |
+
fontSize: '12px',
|
| 88 |
+
lineHeight: '1.35',
|
| 89 |
+
border: '1px solid var(--border-color)',
|
| 90 |
+
background: 'var(--surface-bg)',
|
| 91 |
+
color: 'var(--text-color)',
|
| 92 |
+
boxShadow: '0 4px 24px rgba(0,0,0,.18)',
|
| 93 |
+
opacity: '0',
|
| 94 |
+
transition: 'opacity .12s ease'
|
| 95 |
+
});
|
| 96 |
+
tipInner = document.createElement('div');
|
| 97 |
+
tipInner.className = 'd3-tooltip__inner';
|
| 98 |
+
tipInner.style.textAlign = 'left';
|
| 99 |
+
tip.appendChild(tipInner);
|
| 100 |
+
container.appendChild(tip);
|
| 101 |
+
} else {
|
| 102 |
+
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// SVG scaffolding
|
| 106 |
+
const card = document.createElement('div');
|
| 107 |
+
card.className = 'chart-card';
|
| 108 |
+
container.appendChild(card);
|
| 109 |
+
const svg = d3.select(card).append('svg').attr('width', '100%').style('display', 'block');
|
| 110 |
+
const gRoot = svg.append('g');
|
| 111 |
+
const gCells = gRoot.append('g');
|
| 112 |
+
const gAxes = gRoot.append('g');
|
| 113 |
+
const gColorbar = gRoot.append('g');
|
| 114 |
+
const gColorbarRects = gColorbar.append('g'); // Separate group for colorbar rectangles
|
| 115 |
+
const gColorbarLabels = gColorbar.append('g'); // Separate group for colorbar labels
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
// Data: 6x6 correlation matrix
|
| 119 |
+
const metrics = [
|
| 120 |
+
'LLM score\nconcept',
|
| 121 |
+
'LLM score\ninstruction',
|
| 122 |
+
'LLM score\nfluency',
|
| 123 |
+
'Explicit\nconcept\ninclusion',
|
| 124 |
+
'Log Prob',
|
| 125 |
+
'3-gram\nrepetition'
|
| 126 |
+
];
|
| 127 |
+
|
| 128 |
+
// Correlation matrix (exact values from the image)
|
| 129 |
+
const correlationMatrix = [
|
| 130 |
+
[1.00, -0.28, -0.37, 0.45, -0.57, 0.00],
|
| 131 |
+
[-0.28, 1.00, 0.80, 0.068, 0.45, -0.90],
|
| 132 |
+
[-0.37, 0.80, 1.00, 0.015, 0.67, -0.90],
|
| 133 |
+
[0.45, 0.068, 0.015, 1.00, -0.10, -0.093],
|
| 134 |
+
[-0.57, 0.45, 0.67, -0.10, 1.00, -0.53],
|
| 135 |
+
[0.00, -0.90, -0.90, -0.093, -0.53, 1.00]
|
| 136 |
+
];
|
| 137 |
+
|
| 138 |
+
// Colors: diverging palette via window.ColorPalettes
|
| 139 |
+
const getDivergingColors = (count) => {
|
| 140 |
+
try {
|
| 141 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 142 |
+
const palette = window.ColorPalettes.getColors('diverging', count);
|
| 143 |
+
// Invert the palette: reverse the array
|
| 144 |
+
return palette.slice().reverse();
|
| 145 |
+
}
|
| 146 |
+
} catch (_) {}
|
| 147 |
+
// Fallback: generate diverging scale (blue for negative, red for positive)
|
| 148 |
+
const steps = Math.max(3, count|0);
|
| 149 |
+
const arr = [];
|
| 150 |
+
for (let i = 0; i < steps; i++) {
|
| 151 |
+
const t = i / (steps - 1);
|
| 152 |
+
const pct = Math.round(t * 100);
|
| 153 |
+
// Blue (negative) to Red (positive) via white
|
| 154 |
+
if (t < 0.5) {
|
| 155 |
+
const bluePct = Math.round((0.5 - t) * 200);
|
| 156 |
+
arr.push(`color-mix(in srgb, #3A7BD5 ${bluePct}%, #ffffff ${100-bluePct}%)`);
|
| 157 |
+
} else {
|
| 158 |
+
const redPct = Math.round((t - 0.5) * 200);
|
| 159 |
+
arr.push(`color-mix(in srgb, #ffffff ${100-redPct}%, #D64545 ${redPct}%)`);
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
return arr;
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
const divergingPalette = getDivergingColors(21);
|
| 166 |
+
|
| 167 |
+
let width = 800;
|
| 168 |
+
let height = 600;
|
| 169 |
+
const margin = { top: 40, right: 0, bottom: 0, left: 70 };
|
| 170 |
+
const xLabelHeight = 20; // Height reserved for X-axis labels
|
| 171 |
+
|
| 172 |
+
function updateSize() {
|
| 173 |
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
| 174 |
+
width = container.clientWidth || 800;
|
| 175 |
+
// Use more of the available width and calculate height based on content
|
| 176 |
+
const availableWidth = width;
|
| 177 |
+
const minGridSize = 400;
|
| 178 |
+
// Reserve space for colorbar (80px) and calculate optimal grid size
|
| 179 |
+
const maxGridSize = Math.min(availableWidth - margin.left - margin.right - 80, 800);
|
| 180 |
+
const gridSize = Math.max(minGridSize, maxGridSize);
|
| 181 |
+
// Height must include: top margin + grid + x labels height + extra padding to ensure visibility
|
| 182 |
+
height = margin.top + gridSize + xLabelHeight + 20;
|
| 183 |
+
// Responsive SVG: width 100%, height auto, preserve aspect via viewBox
|
| 184 |
+
svg
|
| 185 |
+
.attr('viewBox', `0 0 ${width} ${height}`)
|
| 186 |
+
.attr('preserveAspectRatio', 'xMidYMid meet')
|
| 187 |
+
.style('width', '100%')
|
| 188 |
+
.style('height', 'auto');
|
| 189 |
+
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 190 |
+
const innerWidth = width - margin.left - margin.right;
|
| 191 |
+
const innerHeight = height - margin.top - margin.bottom;
|
| 192 |
+
return { innerWidth, innerHeight, isDark, gridSize };
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// Compute a fixed readable text color from a CSS rgb()/rgba() string
|
| 196 |
+
function chooseFixedReadableTextOnBg(bgCss){
|
| 197 |
+
try {
|
| 198 |
+
const m = String(bgCss||'').match(/rgba?\(([^)]+)\)/);
|
| 199 |
+
if (!m) return '#0e1116';
|
| 200 |
+
const parts = m[1].split(',').map(s => parseFloat(s.trim()));
|
| 201 |
+
const [r, g, b] = parts;
|
| 202 |
+
// sRGB → relative luminance
|
| 203 |
+
const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255);
|
| 204 |
+
const linear = srgb.map(c => (c <= 0.03928 ? c/12.92 : Math.pow((c + 0.055)/1.055, 2.4)));
|
| 205 |
+
const L = 0.2126*linear[0] + 0.7152*linear[1] + 0.0722*linear[2];
|
| 206 |
+
// Threshold ~ 0.5 for readability; darker BG → white text, else near-black
|
| 207 |
+
return L < 0.5 ? '#ffffff' : '#0e1116';
|
| 208 |
+
} catch(_) { return '#0e1116'; }
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function render() {
|
| 212 |
+
const { innerWidth, innerHeight, gridSize } = updateSize();
|
| 213 |
+
const n = metrics.length;
|
| 214 |
+
const cellSize = gridSize / n;
|
| 215 |
+
|
| 216 |
+
// Ensure xLabelHeight is accessible
|
| 217 |
+
const labelHeight = xLabelHeight;
|
| 218 |
+
|
| 219 |
+
const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0);
|
| 220 |
+
const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0);
|
| 221 |
+
|
| 222 |
+
// Flatten correlation data
|
| 223 |
+
const flatData = [];
|
| 224 |
+
for (let r = 0; r < n; r++) {
|
| 225 |
+
for (let c = 0; c < n; c++) {
|
| 226 |
+
flatData.push({ r, c, value: correlationMatrix[r][c] });
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// Color scale: diverging from -1 to 1
|
| 231 |
+
const colorScale = d3.scaleQuantize()
|
| 232 |
+
.domain([-1, 1])
|
| 233 |
+
.range(divergingPalette);
|
| 234 |
+
|
| 235 |
+
const cells = gCells.selectAll('g.cell')
|
| 236 |
+
.data(flatData, d => `${d.r}-${d.c}`);
|
| 237 |
+
|
| 238 |
+
const cellsEnter = cells.enter()
|
| 239 |
+
.append('g')
|
| 240 |
+
.attr('class', 'cell');
|
| 241 |
+
|
| 242 |
+
cellsEnter.append('rect')
|
| 243 |
+
.attr('rx', 0)
|
| 244 |
+
.attr('ry', 0)
|
| 245 |
+
.on('mousemove', (event, d) => {
|
| 246 |
+
const [px, py] = d3.pointer(event, container);
|
| 247 |
+
tipInner.innerHTML = `<strong>${metrics[d.r]}</strong> × <strong>${metrics[d.c]}</strong><br/>Correlation: ${d.value.toFixed(2)}`;
|
| 248 |
+
tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;
|
| 249 |
+
tip.style.opacity = '1';
|
| 250 |
+
})
|
| 251 |
+
.on('mouseleave', () => {
|
| 252 |
+
tip.style.opacity = '0';
|
| 253 |
+
});
|
| 254 |
+
|
| 255 |
+
cellsEnter.append('text')
|
| 256 |
+
.attr('class', 'cell-text')
|
| 257 |
+
.attr('text-anchor', 'middle')
|
| 258 |
+
.attr('dominant-baseline', 'middle');
|
| 259 |
+
|
| 260 |
+
const cellsMerged = cellsEnter.merge(cells);
|
| 261 |
+
|
| 262 |
+
cellsMerged.select('rect')
|
| 263 |
+
.attr('x', d => x(d.c))
|
| 264 |
+
.attr('y', d => y(d.r))
|
| 265 |
+
.attr('width', Math.max(1, x.bandwidth()))
|
| 266 |
+
.attr('height', Math.max(1, y.bandwidth()))
|
| 267 |
+
.attr('fill', d => colorScale(d.value));
|
| 268 |
+
|
| 269 |
+
cellsMerged.select('text')
|
| 270 |
+
.attr('x', d => x(d.c) + x.bandwidth() / 2)
|
| 271 |
+
.attr('y', d => y(d.r) + y.bandwidth() / 2)
|
| 272 |
+
.text(d => {
|
| 273 |
+
if (d.value === 1.00) return '1';
|
| 274 |
+
const absVal = Math.abs(d.value);
|
| 275 |
+
if (absVal < 0.01) return '0';
|
| 276 |
+
return d.value.toFixed(2);
|
| 277 |
+
})
|
| 278 |
+
.style('fill', function(d){
|
| 279 |
+
try {
|
| 280 |
+
const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;
|
| 281 |
+
const bg = rect ? getComputedStyle(rect).fill : colorScale(d.value);
|
| 282 |
+
return chooseFixedReadableTextOnBg(bg);
|
| 283 |
+
} catch (_) {
|
| 284 |
+
return '#0e1116';
|
| 285 |
+
}
|
| 286 |
+
});
|
| 287 |
+
|
| 288 |
+
cells.exit().remove();
|
| 289 |
+
|
| 290 |
+
// Create clipPath for matrix cells to preserve rounded corners
|
| 291 |
+
const matrixClipId = `matrix-clip-${Math.random().toString(36).slice(2)}`;
|
| 292 |
+
let defsMatrix = svg.select('defs');
|
| 293 |
+
if (defsMatrix.empty()) {
|
| 294 |
+
defsMatrix = svg.append('defs');
|
| 295 |
+
}
|
| 296 |
+
const matrixClipPath = defsMatrix.append('clipPath').attr('id', matrixClipId);
|
| 297 |
+
matrixClipPath.append('rect')
|
| 298 |
+
.attr('x', 0)
|
| 299 |
+
.attr('y', 0)
|
| 300 |
+
.attr('width', gridSize)
|
| 301 |
+
.attr('height', gridSize)
|
| 302 |
+
.attr('rx', 8)
|
| 303 |
+
.attr('ry', 8);
|
| 304 |
+
|
| 305 |
+
// Apply clipPath to cells group
|
| 306 |
+
gCells.attr('clip-path', `url(#${matrixClipId})`);
|
| 307 |
+
|
| 308 |
+
// Draw outer border with rounded corners (in a separate group above cells)
|
| 309 |
+
const gBorder = gRoot.append('g').attr('class', 'matrix-border');
|
| 310 |
+
gBorder.selectAll('rect.cell-bg')
|
| 311 |
+
.data([0])
|
| 312 |
+
.join('rect')
|
| 313 |
+
.attr('class', 'cell-bg')
|
| 314 |
+
.attr('x', 0)
|
| 315 |
+
.attr('y', 0)
|
| 316 |
+
.attr('width', gridSize)
|
| 317 |
+
.attr('height', gridSize)
|
| 318 |
+
.attr('rx', 8)
|
| 319 |
+
.attr('ry', 8)
|
| 320 |
+
.attr('fill', 'none')
|
| 321 |
+
.attr('stroke', 'var(--border-color)')
|
| 322 |
+
.attr('stroke-width', 1);
|
| 323 |
+
|
| 324 |
+
// Axes labels
|
| 325 |
+
gAxes.selectAll('*').remove();
|
| 326 |
+
|
| 327 |
+
// X-axis labels (bottom) - using SVG text with manual line breaks
|
| 328 |
+
const xLabelsGroup = gAxes.append('g').attr('class', 'x-labels');
|
| 329 |
+
const xLabels = xLabelsGroup.selectAll('text')
|
| 330 |
+
.data(metrics)
|
| 331 |
+
.join('text')
|
| 332 |
+
.attr('class', 'axis-label')
|
| 333 |
+
.attr('text-anchor', 'middle')
|
| 334 |
+
.attr('x', (_, i) => x(i) + x.bandwidth() / 2)
|
| 335 |
+
.attr('y', gridSize + 16)
|
| 336 |
+
.style('font-size', '11px')
|
| 337 |
+
.style('font-weight', '700')
|
| 338 |
+
.style('fill', 'var(--text-color)')
|
| 339 |
+
.each(function(d) {
|
| 340 |
+
const text = d3.select(this);
|
| 341 |
+
const lines = d.split('\n');
|
| 342 |
+
lines.forEach((line, i) => {
|
| 343 |
+
text.append('tspan')
|
| 344 |
+
.attr('x', text.attr('x'))
|
| 345 |
+
.attr('dy', i === 0 ? '0' : '1.2em')
|
| 346 |
+
.text(line);
|
| 347 |
+
});
|
| 348 |
+
});
|
| 349 |
+
|
| 350 |
+
// Y-axis labels (left) - using SVG text with manual line breaks
|
| 351 |
+
const yLabelsGroup = gAxes.append('g').attr('class', 'y-labels');
|
| 352 |
+
const yLabels = yLabelsGroup.selectAll('text')
|
| 353 |
+
.data(metrics)
|
| 354 |
+
.join('text')
|
| 355 |
+
.attr('class', 'axis-label')
|
| 356 |
+
.attr('text-anchor', 'end')
|
| 357 |
+
.attr('x', -12)
|
| 358 |
+
.attr('y', (_, i) => y(i) + y.bandwidth() / 2)
|
| 359 |
+
.style('font-size', '11px')
|
| 360 |
+
.style('font-weight', '700')
|
| 361 |
+
.style('fill', 'var(--text-color)')
|
| 362 |
+
.each(function(d) {
|
| 363 |
+
const text = d3.select(this);
|
| 364 |
+
const lines = d.split('\n');
|
| 365 |
+
// Center the text vertically around the y position
|
| 366 |
+
const lineHeight = 1.2;
|
| 367 |
+
const totalHeight = (lines.length - 1) * lineHeight;
|
| 368 |
+
const startY = -totalHeight / 2;
|
| 369 |
+
lines.forEach((line, i) => {
|
| 370 |
+
text.append('tspan')
|
| 371 |
+
.attr('x', text.attr('x'))
|
| 372 |
+
.attr('dy', i === 0 ? startY + 'em' : lineHeight + 'em')
|
| 373 |
+
.attr('text-anchor', 'end')
|
| 374 |
+
.text(line);
|
| 375 |
+
});
|
| 376 |
+
});
|
| 377 |
+
|
| 378 |
+
// Title
|
| 379 |
+
gAxes.append('text')
|
| 380 |
+
.attr('class', 'axis-label')
|
| 381 |
+
.attr('text-anchor', 'middle')
|
| 382 |
+
.attr('x', gridSize / 2)
|
| 383 |
+
.attr('y', -20)
|
| 384 |
+
.style('font-size', '14px')
|
| 385 |
+
.text('Correlation Matrix');
|
| 386 |
+
|
| 387 |
+
// Colorbar
|
| 388 |
+
const colorbarWidth = 20;
|
| 389 |
+
const colorbarHeight = gridSize;
|
| 390 |
+
const colorbarX = gridSize + 20;
|
| 391 |
+
const colorbarY = 0;
|
| 392 |
+
const colorbarSteps = divergingPalette.length;
|
| 393 |
+
const colorbarRadius = 8;
|
| 394 |
+
|
| 395 |
+
// Create clipPath for rounded corners (top and bottom only)
|
| 396 |
+
const clipId = `colorbar-clip-${Math.random().toString(36).slice(2)}`;
|
| 397 |
+
let defsColorbar = svg.select('defs');
|
| 398 |
+
if (defsColorbar.empty()) {
|
| 399 |
+
defsColorbar = svg.append('defs');
|
| 400 |
+
}
|
| 401 |
+
const clipPath = defsColorbar.append('clipPath').attr('id', clipId);
|
| 402 |
+
clipPath.append('rect')
|
| 403 |
+
.attr('x', colorbarX)
|
| 404 |
+
.attr('y', colorbarY)
|
| 405 |
+
.attr('width', colorbarWidth)
|
| 406 |
+
.attr('height', colorbarHeight)
|
| 407 |
+
.attr('rx', colorbarRadius)
|
| 408 |
+
.attr('ry', colorbarRadius);
|
| 409 |
+
|
| 410 |
+
// Apply clipPath only to colorbar rectangles group, not labels
|
| 411 |
+
gColorbarRects.attr('clip-path', `url(#${clipId})`);
|
| 412 |
+
|
| 413 |
+
gColorbarRects.selectAll('rect.colorbar-rect')
|
| 414 |
+
.data(d3.range(colorbarSteps))
|
| 415 |
+
.join('rect')
|
| 416 |
+
.attr('class', 'colorbar-rect')
|
| 417 |
+
.attr('x', colorbarX)
|
| 418 |
+
.attr('y', (_, i) => colorbarY + (colorbarHeight / colorbarSteps) * i)
|
| 419 |
+
.attr('width', colorbarWidth)
|
| 420 |
+
.attr('height', colorbarHeight / colorbarSteps)
|
| 421 |
+
.attr('fill', (_, i) => divergingPalette[colorbarSteps - 1 - i])
|
| 422 |
+
.attr('stroke', 'none');
|
| 423 |
+
|
| 424 |
+
// Colorbar border with rounded corners (top and bottom)
|
| 425 |
+
gColorbarRects.append('rect')
|
| 426 |
+
.attr('x', colorbarX)
|
| 427 |
+
.attr('y', colorbarY)
|
| 428 |
+
.attr('width', colorbarWidth)
|
| 429 |
+
.attr('height', colorbarHeight)
|
| 430 |
+
.attr('rx', colorbarRadius)
|
| 431 |
+
.attr('ry', colorbarRadius)
|
| 432 |
+
.attr('fill', 'none')
|
| 433 |
+
.attr('stroke', 'var(--border-color)')
|
| 434 |
+
.attr('stroke-width', 1);
|
| 435 |
+
|
| 436 |
+
// Colorbar labels (outside clipPath so they're visible)
|
| 437 |
+
const colorbarTicks = [-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1];
|
| 438 |
+
gColorbarLabels.selectAll('text.colorbar-tick')
|
| 439 |
+
.data(colorbarTicks)
|
| 440 |
+
.join('text')
|
| 441 |
+
.attr('class', 'colorbar-label')
|
| 442 |
+
.attr('text-anchor', 'start')
|
| 443 |
+
.attr('x', colorbarX + colorbarWidth + 6)
|
| 444 |
+
.attr('y', d => colorbarY + (1 - d) / 2 * colorbarHeight)
|
| 445 |
+
.attr('dominant-baseline', 'middle')
|
| 446 |
+
.text(d => d.toFixed(2));
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
// Initial render + resize handling
|
| 450 |
+
const rerender = () => render();
|
| 451 |
+
if (window.ResizeObserver) {
|
| 452 |
+
const ro = new ResizeObserver(() => rerender());
|
| 453 |
+
ro.observe(container);
|
| 454 |
+
} else {
|
| 455 |
+
window.addEventListener('resize', rerender);
|
| 456 |
+
}
|
| 457 |
+
};
|
| 458 |
+
|
| 459 |
+
if (document.readyState === 'loading') {
|
| 460 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 461 |
+
} else {
|
| 462 |
+
ensureD3(bootstrap);
|
| 463 |
+
}
|
| 464 |
+
})();
|
| 465 |
+
</script>
|
| 466 |
+
|
app/src/content/embeds/{d3-evaluation1-naive.html → d3-evaluation-configurable.html}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
<div class="d3-eval-grid d3-eval-grid-
|
| 2 |
<style>
|
| 3 |
.d3-eval-grid {
|
| 4 |
padding: 2px;
|
|
@@ -91,11 +91,18 @@
|
|
| 91 |
}, { once: true });
|
| 92 |
};
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
const bootstrap = () => {
|
| 95 |
const scriptEl = document.currentScript;
|
| 96 |
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 97 |
-
if (!(container && container.classList && container.classList.contains('d3-eval-grid-
|
| 98 |
-
const candidates = Array.from(document.querySelectorAll('.d3-eval-grid-
|
| 99 |
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
|
| 100 |
container = candidates[candidates.length - 1] || null;
|
| 101 |
}
|
|
@@ -105,8 +112,32 @@
|
|
| 105 |
container.dataset.mounted = 'true';
|
| 106 |
}
|
| 107 |
|
| 108 |
-
// Find
|
| 109 |
let mountEl = container;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
|
| 111 |
mountEl = mountEl.parentElement;
|
| 112 |
}
|
|
@@ -118,15 +149,6 @@
|
|
| 118 |
}
|
| 119 |
} catch(_) {}
|
| 120 |
|
| 121 |
-
// Check for experiments filter attribute
|
| 122 |
-
let experimentsFilter = null;
|
| 123 |
-
try {
|
| 124 |
-
const expAttr = container.getAttribute('data-experiments');
|
| 125 |
-
if (expAttr) {
|
| 126 |
-
experimentsFilter = JSON.parse(expAttr);
|
| 127 |
-
}
|
| 128 |
-
} catch(_) {}
|
| 129 |
-
|
| 130 |
const DEFAULT_JSON = '/data/evaluation_summary.json';
|
| 131 |
const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
|
| 132 |
|
|
@@ -151,9 +173,8 @@
|
|
| 151 |
|
| 152 |
fetchFirstAvailable(JSON_PATHS)
|
| 153 |
.then(rawData => {
|
| 154 |
-
//
|
| 155 |
const allExperiments = ['Prompt', 'Basic steering', 'Clamping', 'Clamping + Penalty', '2D optimized', '8D optimized'];
|
| 156 |
-
const visibleExperiments = ['Prompt', 'Basic steering'];
|
| 157 |
|
| 158 |
// Metrics in 2x4 grid layout (8 metrics)
|
| 159 |
const metrics = [
|
|
@@ -174,14 +195,177 @@
|
|
| 174 |
data[d.metric][d.experiment] = { mean: d.mean, std: d.std };
|
| 175 |
});
|
| 176 |
|
| 177 |
-
// Color palette -
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
const allColors = {
|
| 179 |
-
'Prompt': '#4c4c4c',
|
| 180 |
-
'Basic steering': '#
|
| 181 |
-
'Clamping': '#
|
| 182 |
-
'Clamping + Penalty': '#
|
| 183 |
-
'2D optimized': '#
|
| 184 |
-
'8D optimized': '#
|
| 185 |
};
|
| 186 |
|
| 187 |
const gridContainer = document.createElement('div');
|
|
@@ -412,3 +596,4 @@
|
|
| 412 |
}
|
| 413 |
})();
|
| 414 |
</script>
|
|
|
|
|
|
| 1 |
+
<div class="d3-eval-grid d3-eval-grid-configurable"></div>
|
| 2 |
<style>
|
| 3 |
.d3-eval-grid {
|
| 4 |
padding: 2px;
|
|
|
|
| 91 |
}, { once: true });
|
| 92 |
};
|
| 93 |
|
| 94 |
+
// Define experiment states
|
| 95 |
+
const EXPERIMENT_STATES = {
|
| 96 |
+
'naive': ['Prompt', 'Basic steering'],
|
| 97 |
+
'clamp': ['Prompt', 'Basic steering', 'Clamping', 'Clamping + Penalty'],
|
| 98 |
+
'multi': ['Prompt', 'Basic steering', 'Clamping', 'Clamping + Penalty', '2D optimized', '8D optimized']
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
const bootstrap = () => {
|
| 102 |
const scriptEl = document.currentScript;
|
| 103 |
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 104 |
+
if (!(container && container.classList && container.classList.contains('d3-eval-grid-configurable'))) {
|
| 105 |
+
const candidates = Array.from(document.querySelectorAll('.d3-eval-grid-configurable'))
|
| 106 |
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
|
| 107 |
container = candidates[candidates.length - 1] || null;
|
| 108 |
}
|
|
|
|
| 112 |
container.dataset.mounted = 'true';
|
| 113 |
}
|
| 114 |
|
| 115 |
+
// Find config attribute
|
| 116 |
let mountEl = container;
|
| 117 |
+
let configValue = null;
|
| 118 |
+
while (mountEl && !mountEl.getAttribute?.('data-config')) {
|
| 119 |
+
mountEl = mountEl.parentElement;
|
| 120 |
+
}
|
| 121 |
+
try {
|
| 122 |
+
const configAttr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
|
| 123 |
+
if (configAttr && configAttr.trim()) {
|
| 124 |
+
// Try to parse as JSON first, otherwise treat as string
|
| 125 |
+
try {
|
| 126 |
+
configValue = JSON.parse(configAttr);
|
| 127 |
+
} catch(_) {
|
| 128 |
+
configValue = configAttr.trim();
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
} catch(_) {}
|
| 132 |
+
|
| 133 |
+
// Determine visible experiments based on config
|
| 134 |
+
// Default to 'naive' if no config provided
|
| 135 |
+
const stateName = typeof configValue === 'string' ? configValue.toLowerCase() :
|
| 136 |
+
(configValue && configValue.state) ? configValue.state.toLowerCase() : 'naive';
|
| 137 |
+
const visibleExperiments = EXPERIMENT_STATES[stateName] || EXPERIMENT_STATES['naive'];
|
| 138 |
+
|
| 139 |
+
// Find data attribute
|
| 140 |
+
mountEl = container;
|
| 141 |
while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
|
| 142 |
mountEl = mountEl.parentElement;
|
| 143 |
}
|
|
|
|
| 149 |
}
|
| 150 |
} catch(_) {}
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
const DEFAULT_JSON = '/data/evaluation_summary.json';
|
| 153 |
const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
|
| 154 |
|
|
|
|
| 173 |
|
| 174 |
fetchFirstAvailable(JSON_PATHS)
|
| 175 |
.then(rawData => {
|
| 176 |
+
// All experiments for consistent positioning
|
| 177 |
const allExperiments = ['Prompt', 'Basic steering', 'Clamping', 'Clamping + Penalty', '2D optimized', '8D optimized'];
|
|
|
|
| 178 |
|
| 179 |
// Metrics in 2x4 grid layout (8 metrics)
|
| 180 |
const metrics = [
|
|
|
|
| 195 |
data[d.metric][d.experiment] = { mean: d.mean, std: d.std };
|
| 196 |
});
|
| 197 |
|
| 198 |
+
// Color palette - use categorical colors with similar hues for related experiments
|
| 199 |
+
const getCategoricalColors = (count) => {
|
| 200 |
+
try {
|
| 201 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 202 |
+
return window.ColorPalettes.getColors('categorical', count);
|
| 203 |
+
}
|
| 204 |
+
} catch (_) {}
|
| 205 |
+
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 206 |
+
const tableau = (window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ab'];
|
| 207 |
+
const pool = [primary, ...tableau];
|
| 208 |
+
const arr = []; for (let i = 0; i < count; i++) { arr.push(pool[i % pool.length]); }
|
| 209 |
+
return arr;
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
// Get base colors for groups - start with primary color for Basic steering
|
| 213 |
+
const baseColors = getCategoricalColors(3);
|
| 214 |
+
|
| 215 |
+
// Create variations using sequential palette for similar hues - more harmonious
|
| 216 |
+
const getSequentialVariations = (baseColor, count) => {
|
| 217 |
+
try {
|
| 218 |
+
// Try to use ColorPalettes sequential generator if available
|
| 219 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getPrimaryOKLCH === 'function') {
|
| 220 |
+
// Parse base color to extract its hue
|
| 221 |
+
const parseColor = (color) => {
|
| 222 |
+
const el = document.createElement('span');
|
| 223 |
+
el.style.color = color;
|
| 224 |
+
document.body.appendChild(el);
|
| 225 |
+
const rgb = getComputedStyle(el).color.match(/\d+/g);
|
| 226 |
+
document.body.removeChild(el);
|
| 227 |
+
if (!rgb || rgb.length < 3) return null;
|
| 228 |
+
return { r: rgb[0]/255, g: rgb[1]/255, b: rgb[2]/255 };
|
| 229 |
+
};
|
| 230 |
+
|
| 231 |
+
const rgb = parseColor(baseColor);
|
| 232 |
+
if (!rgb) return Array(count).fill(baseColor);
|
| 233 |
+
|
| 234 |
+
// Convert RGB to HSL to get hue
|
| 235 |
+
const max = Math.max(rgb.r, rgb.g, rgb.b);
|
| 236 |
+
const min = Math.min(rgb.r, rgb.g, rgb.b);
|
| 237 |
+
const delta = max - min;
|
| 238 |
+
let h = 0;
|
| 239 |
+
if (delta !== 0) {
|
| 240 |
+
if (max === rgb.r) h = ((rgb.g - rgb.b) / delta) % 6;
|
| 241 |
+
else if (max === rgb.g) h = (rgb.b - rgb.r) / delta + 2;
|
| 242 |
+
else h = (rgb.r - rgb.g) / delta + 4;
|
| 243 |
+
}
|
| 244 |
+
h = h * 60;
|
| 245 |
+
if (h < 0) h += 360;
|
| 246 |
+
|
| 247 |
+
// Get primary OKLCH to use as base for sequential generation
|
| 248 |
+
const primaryOKLCH = window.ColorPalettes.getPrimaryOKLCH();
|
| 249 |
+
if (primaryOKLCH) {
|
| 250 |
+
// Create a temporary OKLCH color with the base color's hue
|
| 251 |
+
// Use the primary's L and C as reference, but use the base color's hue
|
| 252 |
+
const baseL = 0.65; // Medium lightness
|
| 253 |
+
const baseC = 0.2; // Medium chroma
|
| 254 |
+
|
| 255 |
+
// Generate sequential palette with the base color's hue
|
| 256 |
+
// This creates harmonious variations
|
| 257 |
+
const tempOKLCH = { L: baseL, C: baseC, h: h };
|
| 258 |
+
|
| 259 |
+
// Use ColorPalettes sequential generator if we can create a custom one
|
| 260 |
+
// Otherwise, create subtle variations manually
|
| 261 |
+
const variations = [];
|
| 262 |
+
for (let i = 0; i < count; i++) {
|
| 263 |
+
const t = count === 1 ? 0.5 : i / (count - 1);
|
| 264 |
+
// More subtle variation: smaller range for harmony
|
| 265 |
+
const LVar = baseL + (t - 0.5) * 0.12; // Range: 0.59 to 0.71 (more subtle)
|
| 266 |
+
const CVar = baseC * (0.95 + t * 0.1); // Slight saturation variation
|
| 267 |
+
|
| 268 |
+
// Use ColorPalettes oklchToHexSafe if available
|
| 269 |
+
if (window.ColorPalettes && window.ColorPalettes.getColors) {
|
| 270 |
+
// Try to get sequential colors and adjust hue
|
| 271 |
+
// For now, use manual HSL conversion with better parameters
|
| 272 |
+
const light = Math.max(0.5, Math.min(0.75, LVar));
|
| 273 |
+
const sat = Math.max(0.35, Math.min(0.55, CVar * 2.5));
|
| 274 |
+
const hue = h / 360;
|
| 275 |
+
const c = sat * (1 - Math.abs(2 * light - 1));
|
| 276 |
+
const x = c * (1 - Math.abs((hue * 6) % 2 - 1));
|
| 277 |
+
const m = light - c / 2;
|
| 278 |
+
let r, g, b;
|
| 279 |
+
if (hue < 1/6) { r = c; g = x; b = 0; }
|
| 280 |
+
else if (hue < 2/6) { r = x; g = c; b = 0; }
|
| 281 |
+
else if (hue < 3/6) { r = 0; g = c; b = x; }
|
| 282 |
+
else if (hue < 4/6) { r = 0; g = x; b = c; }
|
| 283 |
+
else if (hue < 5/6) { r = x; g = 0; b = c; }
|
| 284 |
+
else { r = c; g = 0; b = x; }
|
| 285 |
+
const toHex = (n) => Math.round(Math.max(0, Math.min(255, (n + m) * 255))).toString(16).padStart(2, '0').toUpperCase();
|
| 286 |
+
variations.push(`#${toHex(r)}${toHex(g)}${toHex(b)}`);
|
| 287 |
+
} else {
|
| 288 |
+
variations.push(baseColor);
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
return variations;
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
} catch (_) {}
|
| 295 |
+
|
| 296 |
+
// Fallback: create subtle variations manually
|
| 297 |
+
try {
|
| 298 |
+
const parseColor = (color) => {
|
| 299 |
+
const el = document.createElement('span');
|
| 300 |
+
el.style.color = color;
|
| 301 |
+
document.body.appendChild(el);
|
| 302 |
+
const rgb = getComputedStyle(el).color.match(/\d+/g);
|
| 303 |
+
document.body.removeChild(el);
|
| 304 |
+
if (!rgb || rgb.length < 3) return null;
|
| 305 |
+
return { r: rgb[0]/255, g: rgb[1]/255, b: rgb[2]/255 };
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
const rgb = parseColor(baseColor);
|
| 309 |
+
if (!rgb) return Array(count).fill(baseColor);
|
| 310 |
+
|
| 311 |
+
// Convert RGB to HSL
|
| 312 |
+
const max = Math.max(rgb.r, rgb.g, rgb.b);
|
| 313 |
+
const min = Math.min(rgb.r, rgb.g, rgb.b);
|
| 314 |
+
const delta = max - min;
|
| 315 |
+
let h = 0;
|
| 316 |
+
if (delta !== 0) {
|
| 317 |
+
if (max === rgb.r) h = ((rgb.g - rgb.b) / delta) % 6;
|
| 318 |
+
else if (max === rgb.g) h = (rgb.b - rgb.r) / delta + 2;
|
| 319 |
+
else h = (rgb.r - rgb.g) / delta + 4;
|
| 320 |
+
}
|
| 321 |
+
h = h * 60;
|
| 322 |
+
if (h < 0) h += 360;
|
| 323 |
+
|
| 324 |
+
// More harmonious variations: smaller, subtler changes
|
| 325 |
+
const variations = [];
|
| 326 |
+
for (let i = 0; i < count; i++) {
|
| 327 |
+
const t = count === 1 ? 0.5 : i / (count - 1);
|
| 328 |
+
// Subtle lightness variation: smaller range for harmony
|
| 329 |
+
const light = 0.6 + (t - 0.5) * 0.15; // Range: 0.525 to 0.675 (more subtle)
|
| 330 |
+
const sat = 0.45 + t * 0.15; // Range: 0.45 to 0.60 (more controlled)
|
| 331 |
+
|
| 332 |
+
// Convert HSL to RGB
|
| 333 |
+
const hue = h / 360;
|
| 334 |
+
const c = sat * (1 - Math.abs(2 * light - 1));
|
| 335 |
+
const x = c * (1 - Math.abs((hue * 6) % 2 - 1));
|
| 336 |
+
const m = light - c / 2;
|
| 337 |
+
let r, g, b;
|
| 338 |
+
if (hue < 1/6) { r = c; g = x; b = 0; }
|
| 339 |
+
else if (hue < 2/6) { r = x; g = c; b = 0; }
|
| 340 |
+
else if (hue < 3/6) { r = 0; g = c; b = x; }
|
| 341 |
+
else if (hue < 4/6) { r = 0; g = x; b = c; }
|
| 342 |
+
else if (hue < 5/6) { r = x; g = 0; b = c; }
|
| 343 |
+
else { r = c; g = 0; b = x; }
|
| 344 |
+
const toHex = (n) => Math.round(Math.max(0, Math.min(255, (n + m) * 255))).toString(16).padStart(2, '0').toUpperCase();
|
| 345 |
+
variations.push(`#${toHex(r)}${toHex(g)}${toHex(b)}`);
|
| 346 |
+
}
|
| 347 |
+
return variations;
|
| 348 |
+
} catch (_) {
|
| 349 |
+
return Array(count).fill(baseColor);
|
| 350 |
+
}
|
| 351 |
+
};
|
| 352 |
+
|
| 353 |
+
// Create color groups
|
| 354 |
+
// baseColors[0] is primary color (first in categorical palette)
|
| 355 |
+
// baseColors[1] is second color
|
| 356 |
+
// baseColors[2] is third color
|
| 357 |
+
const clampBase = baseColors[1] || '#4e79a7'; // Second color for clamping group
|
| 358 |
+
const optimizedBase = baseColors[2] || '#59a14f'; // Third color for optimized group
|
| 359 |
+
const clampVariations = getSequentialVariations(clampBase, 2);
|
| 360 |
+
const optimizedVariations = getSequentialVariations(optimizedBase, 2);
|
| 361 |
+
|
| 362 |
const allColors = {
|
| 363 |
+
'Prompt': '#4c4c4c', // Keep gray for baseline/reference
|
| 364 |
+
'Basic steering': baseColors[0] || '#E889AB', // Primary color (first in palette)
|
| 365 |
+
'Clamping': clampVariations[0] || '#4e79a7',
|
| 366 |
+
'Clamping + Penalty': clampVariations[1] || '#5a8ab8',
|
| 367 |
+
'2D optimized': optimizedVariations[0] || '#59a14f',
|
| 368 |
+
'8D optimized': optimizedVariations[1] || '#6bb26b'
|
| 369 |
};
|
| 370 |
|
| 371 |
const gridContainer = document.createElement('div');
|
|
|
|
| 596 |
}
|
| 597 |
})();
|
| 598 |
</script>
|
| 599 |
+
|
app/src/content/embeds/d3-evaluation-grid.html
DELETED
|
@@ -1,467 +0,0 @@
|
|
| 1 |
-
<div class="d3-eval-grid"></div>
|
| 2 |
-
<style>
|
| 3 |
-
.d3-eval-grid {
|
| 4 |
-
padding: 8px;
|
| 5 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
.d3-eval-grid .chart-card {
|
| 9 |
-
background: var(--surface-bg);
|
| 10 |
-
border: 1px solid var(--border-color);
|
| 11 |
-
border-radius: 10px;
|
| 12 |
-
padding: 16px;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
.d3-eval-grid .grid-container {
|
| 16 |
-
display: grid;
|
| 17 |
-
grid-template-columns: repeat(2, 1fr);
|
| 18 |
-
gap: 24px;
|
| 19 |
-
margin-bottom: 16px;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
@media (max-width: 768px) {
|
| 23 |
-
.d3-eval-grid .grid-container {
|
| 24 |
-
grid-template-columns: 1fr;
|
| 25 |
-
}
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
.d3-eval-grid .subplot {
|
| 29 |
-
background: var(--surface-bg);
|
| 30 |
-
border: 1px solid var(--border-color);
|
| 31 |
-
border-radius: 8px;
|
| 32 |
-
padding: 12px;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
.d3-eval-grid .subplot-title {
|
| 36 |
-
font-size: 13px;
|
| 37 |
-
font-weight: 600;
|
| 38 |
-
color: var(--text-color);
|
| 39 |
-
margin-bottom: 8px;
|
| 40 |
-
text-align: center;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
.d3-eval-grid .legend {
|
| 44 |
-
display: flex;
|
| 45 |
-
flex-wrap: wrap;
|
| 46 |
-
gap: 8px 16px;
|
| 47 |
-
padding-top: 12px;
|
| 48 |
-
border-top: 1px solid var(--border-color);
|
| 49 |
-
font-size: 12px;
|
| 50 |
-
justify-content: center;
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
.d3-eval-grid .legend-item {
|
| 54 |
-
display: flex;
|
| 55 |
-
align-items: center;
|
| 56 |
-
gap: 6px;
|
| 57 |
-
cursor: pointer;
|
| 58 |
-
transition: opacity 0.2s;
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
.d3-eval-grid .legend-item.dimmed {
|
| 62 |
-
opacity: 0.3;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
.d3-eval-grid .legend-swatch {
|
| 66 |
-
width: 14px;
|
| 67 |
-
height: 14px;
|
| 68 |
-
border-radius: 3px;
|
| 69 |
-
border: 1px solid var(--border-color);
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
.d3-eval-grid .axes path,
|
| 73 |
-
.d3-eval-grid .axes line {
|
| 74 |
-
stroke: var(--axis-color);
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
.d3-eval-grid .axes text {
|
| 78 |
-
fill: var(--tick-color);
|
| 79 |
-
font-size: 10px;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
.d3-eval-grid .grid line {
|
| 83 |
-
stroke: var(--grid-color);
|
| 84 |
-
stroke-dasharray: 2,2;
|
| 85 |
-
opacity: 0.5;
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
.d3-eval-grid .axis-label {
|
| 89 |
-
fill: var(--text-color);
|
| 90 |
-
font-size: 11px;
|
| 91 |
-
font-weight: 600;
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
.d3-eval-grid .d3-tooltip {
|
| 95 |
-
position: absolute;
|
| 96 |
-
pointer-events: none;
|
| 97 |
-
padding: 8px 10px;
|
| 98 |
-
background: var(--surface-bg);
|
| 99 |
-
border: 1px solid var(--border-color);
|
| 100 |
-
border-radius: 8px;
|
| 101 |
-
font-size: 11px;
|
| 102 |
-
line-height: 1.5;
|
| 103 |
-
box-shadow: 0 4px 24px rgba(0,0,0,.18);
|
| 104 |
-
opacity: 0;
|
| 105 |
-
transition: opacity 0.2s;
|
| 106 |
-
z-index: 1000;
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
.d3-eval-grid .bar {
|
| 110 |
-
transition: opacity 0.2s;
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
.d3-eval-grid .bar.dimmed {
|
| 114 |
-
opacity: 0.2;
|
| 115 |
-
}
|
| 116 |
-
</style>
|
| 117 |
-
<script>
|
| 118 |
-
(() => {
|
| 119 |
-
const ensureD3 = (cb) => {
|
| 120 |
-
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 121 |
-
let s = document.getElementById('d3-cdn-script');
|
| 122 |
-
if (!s) {
|
| 123 |
-
s = document.createElement('script');
|
| 124 |
-
s.id = 'd3-cdn-script';
|
| 125 |
-
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 126 |
-
document.head.appendChild(s);
|
| 127 |
-
}
|
| 128 |
-
s.addEventListener('load', () => {
|
| 129 |
-
if (window.d3 && typeof window.d3.select === 'function') cb();
|
| 130 |
-
}, { once: true });
|
| 131 |
-
};
|
| 132 |
-
|
| 133 |
-
const bootstrap = () => {
|
| 134 |
-
const scriptEl = document.currentScript;
|
| 135 |
-
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 136 |
-
if (!(container && container.classList && container.classList.contains('d3-eval-grid'))) {
|
| 137 |
-
const candidates = Array.from(document.querySelectorAll('.d3-eval-grid'))
|
| 138 |
-
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
|
| 139 |
-
container = candidates[candidates.length - 1] || null;
|
| 140 |
-
}
|
| 141 |
-
if (!container) return;
|
| 142 |
-
if (container.dataset) {
|
| 143 |
-
if (container.dataset.mounted === 'true') return;
|
| 144 |
-
container.dataset.mounted = 'true';
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
// Find data attribute
|
| 148 |
-
let mountEl = container;
|
| 149 |
-
while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
|
| 150 |
-
mountEl = mountEl.parentElement;
|
| 151 |
-
}
|
| 152 |
-
let providedData = null;
|
| 153 |
-
try {
|
| 154 |
-
const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
|
| 155 |
-
if (attr && attr.trim()) {
|
| 156 |
-
providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim();
|
| 157 |
-
}
|
| 158 |
-
} catch(_) {}
|
| 159 |
-
|
| 160 |
-
// Check for experiments filter attribute
|
| 161 |
-
let experimentsFilter = null;
|
| 162 |
-
try {
|
| 163 |
-
const expAttr = container.getAttribute('data-experiments');
|
| 164 |
-
if (expAttr) {
|
| 165 |
-
experimentsFilter = JSON.parse(expAttr);
|
| 166 |
-
}
|
| 167 |
-
} catch(_) {}
|
| 168 |
-
|
| 169 |
-
const DEFAULT_JSON = '/data/evaluation_summary.json';
|
| 170 |
-
const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
|
| 171 |
-
|
| 172 |
-
const JSON_PATHS = typeof providedData === 'string'
|
| 173 |
-
? [ensureDataPrefix(providedData)]
|
| 174 |
-
: [
|
| 175 |
-
DEFAULT_JSON,
|
| 176 |
-
'./assets/data/evaluation_summary.json',
|
| 177 |
-
'../assets/data/evaluation_summary.json',
|
| 178 |
-
'../../assets/data/evaluation_summary.json'
|
| 179 |
-
];
|
| 180 |
-
|
| 181 |
-
const fetchFirstAvailable = async (paths) => {
|
| 182 |
-
for (const p of paths) {
|
| 183 |
-
try {
|
| 184 |
-
const r = await fetch(p, { cache: 'no-cache' });
|
| 185 |
-
if (r.ok) return await r.json();
|
| 186 |
-
} catch(_){}
|
| 187 |
-
}
|
| 188 |
-
throw new Error('JSON not found');
|
| 189 |
-
};
|
| 190 |
-
|
| 191 |
-
fetchFirstAvailable(JSON_PATHS)
|
| 192 |
-
.then(rawData => {
|
| 193 |
-
// All experiments in order
|
| 194 |
-
const allExperiments = ['Prompt', 'Basic steering', 'Clamping', 'Clamping + Penalty', '2D optimized', '8D optimized'];
|
| 195 |
-
|
| 196 |
-
// Use filtered experiments if provided, otherwise use all
|
| 197 |
-
const experiments = experimentsFilter || allExperiments;
|
| 198 |
-
|
| 199 |
-
// Metrics in 2x3 grid layout
|
| 200 |
-
const metrics = [
|
| 201 |
-
{ key: 'llm_score_concept', label: 'LLM Concept Score', format: d3.format('.2f') },
|
| 202 |
-
{ key: 'llm_score_instruction', label: 'LLM Instruction Score', format: d3.format('.2f') },
|
| 203 |
-
{ key: 'llm_score_fluency', label: 'LLM Fluency Score', format: d3.format('.2f') },
|
| 204 |
-
{ key: 'rep3', label: '3-gram Repetition Fraction', format: d3.format('.2f') },
|
| 205 |
-
{ key: 'mean_llm_score', label: 'Mean LLM Score', format: d3.format('.2f') },
|
| 206 |
-
{ key: 'harmonic_llm_score', label: 'Harmonic Mean LLM Score', format: d3.format('.2f') }
|
| 207 |
-
];
|
| 208 |
-
|
| 209 |
-
// Restructure data
|
| 210 |
-
const data = {};
|
| 211 |
-
rawData.forEach(d => {
|
| 212 |
-
if (!data[d.metric]) data[d.metric] = {};
|
| 213 |
-
data[d.metric][d.experiment] = { mean: d.mean, std: d.std };
|
| 214 |
-
});
|
| 215 |
-
|
| 216 |
-
// Color palette - consistent across all charts
|
| 217 |
-
const allColors = {
|
| 218 |
-
'Prompt': '#4c4c4c',
|
| 219 |
-
'Basic steering': '#b2b2b2',
|
| 220 |
-
'Clamping': '#b2b2cc',
|
| 221 |
-
'Clamping + Penalty': '#b2b2e6',
|
| 222 |
-
'2D optimized': '#b2ffb2',
|
| 223 |
-
'8D optimized': '#ffb2ff'
|
| 224 |
-
};
|
| 225 |
-
|
| 226 |
-
const card = document.createElement('div');
|
| 227 |
-
card.className = 'chart-card';
|
| 228 |
-
container.appendChild(card);
|
| 229 |
-
|
| 230 |
-
const gridContainer = document.createElement('div');
|
| 231 |
-
gridContainer.className = 'grid-container';
|
| 232 |
-
card.appendChild(gridContainer);
|
| 233 |
-
|
| 234 |
-
// Tooltip
|
| 235 |
-
const tooltip = d3.select(card).append('div')
|
| 236 |
-
.attr('class', 'd3-tooltip')
|
| 237 |
-
.style('transform', 'translate(-9999px, -9999px)');
|
| 238 |
-
|
| 239 |
-
let hoveredExperiment = null;
|
| 240 |
-
|
| 241 |
-
// Create each subplot
|
| 242 |
-
metrics.forEach((metric, idx) => {
|
| 243 |
-
const subplot = document.createElement('div');
|
| 244 |
-
subplot.className = 'subplot';
|
| 245 |
-
subplot.dataset.metric = metric.key;
|
| 246 |
-
gridContainer.appendChild(subplot);
|
| 247 |
-
|
| 248 |
-
const title = document.createElement('div');
|
| 249 |
-
title.className = 'subplot-title';
|
| 250 |
-
title.textContent = metric.label;
|
| 251 |
-
subplot.appendChild(title);
|
| 252 |
-
|
| 253 |
-
const svg = d3.select(subplot).append('svg')
|
| 254 |
-
.attr('width', '100%')
|
| 255 |
-
.style('display', 'block');
|
| 256 |
-
|
| 257 |
-
const g = svg.append('g');
|
| 258 |
-
const gGrid = g.append('g').attr('class', 'grid');
|
| 259 |
-
const gBars = g.append('g').attr('class', 'bars');
|
| 260 |
-
const gErrorBars = g.append('g').attr('class', 'error-bars');
|
| 261 |
-
const gAxes = g.append('g').attr('class', 'axes');
|
| 262 |
-
|
| 263 |
-
subplot._render = () => {
|
| 264 |
-
const width = subplot.clientWidth || 300;
|
| 265 |
-
const height = Math.max(200, Math.round(width * 0.6));
|
| 266 |
-
const margin = { top: 10, right: 10, bottom: 60, left: 50 };
|
| 267 |
-
const innerWidth = width - margin.left - margin.right;
|
| 268 |
-
const innerHeight = height - margin.top - margin.bottom;
|
| 269 |
-
|
| 270 |
-
svg.attr('height', height);
|
| 271 |
-
g.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 272 |
-
|
| 273 |
-
// Scales
|
| 274 |
-
const x = d3.scaleBand()
|
| 275 |
-
.domain(experiments)
|
| 276 |
-
.range([0, innerWidth])
|
| 277 |
-
.padding(0.2);
|
| 278 |
-
|
| 279 |
-
// Find y domain for this metric
|
| 280 |
-
const values = experiments.map(exp => data[metric.key]?.[exp]?.mean).filter(v => v !== undefined);
|
| 281 |
-
const stds = experiments.map(exp => data[metric.key]?.[exp]?.std).filter(v => v !== undefined);
|
| 282 |
-
const maxVal = d3.max(values.map((v, i) => v + stds[i]));
|
| 283 |
-
const minVal = d3.min(values.map((v, i) => Math.max(0, v - stds[i])));
|
| 284 |
-
|
| 285 |
-
const y = d3.scaleLinear()
|
| 286 |
-
.domain([Math.max(0, minVal * 0.95), maxVal * 1.05])
|
| 287 |
-
.range([innerHeight, 0])
|
| 288 |
-
.nice();
|
| 289 |
-
|
| 290 |
-
// Grid
|
| 291 |
-
gGrid.selectAll('*').remove();
|
| 292 |
-
gGrid.selectAll('line')
|
| 293 |
-
.data(y.ticks(4))
|
| 294 |
-
.join('line')
|
| 295 |
-
.attr('x1', 0)
|
| 296 |
-
.attr('x2', innerWidth)
|
| 297 |
-
.attr('y1', d => y(d))
|
| 298 |
-
.attr('y2', d => y(d));
|
| 299 |
-
|
| 300 |
-
// Axes
|
| 301 |
-
gAxes.selectAll('*').remove();
|
| 302 |
-
|
| 303 |
-
const xAxis = gAxes.append('g')
|
| 304 |
-
.attr('transform', `translate(0,${innerHeight})`)
|
| 305 |
-
.call(d3.axisBottom(x).tickSize(3));
|
| 306 |
-
|
| 307 |
-
xAxis.selectAll('text')
|
| 308 |
-
.attr('transform', 'rotate(-45)')
|
| 309 |
-
.style('text-anchor', 'end')
|
| 310 |
-
.attr('dx', '-0.5em')
|
| 311 |
-
.attr('dy', '0.15em');
|
| 312 |
-
|
| 313 |
-
gAxes.append('g')
|
| 314 |
-
.call(d3.axisLeft(y).ticks(4).tickFormat(metric.format).tickSize(3));
|
| 315 |
-
|
| 316 |
-
// Draw bars
|
| 317 |
-
const bars = [];
|
| 318 |
-
experiments.forEach(exp => {
|
| 319 |
-
const d = data[metric.key]?.[exp];
|
| 320 |
-
if (d) {
|
| 321 |
-
bars.push({
|
| 322 |
-
experiment: exp,
|
| 323 |
-
mean: d.mean,
|
| 324 |
-
std: d.std,
|
| 325 |
-
color: allColors[exp],
|
| 326 |
-
x: x(exp),
|
| 327 |
-
y: y(d.mean),
|
| 328 |
-
width: x.bandwidth(),
|
| 329 |
-
height: innerHeight - y(d.mean)
|
| 330 |
-
});
|
| 331 |
-
}
|
| 332 |
-
});
|
| 333 |
-
|
| 334 |
-
gBars.selectAll('rect')
|
| 335 |
-
.data(bars)
|
| 336 |
-
.join('rect')
|
| 337 |
-
.attr('class', 'bar')
|
| 338 |
-
.attr('x', d => d.x)
|
| 339 |
-
.attr('y', d => d.y)
|
| 340 |
-
.attr('width', d => d.width)
|
| 341 |
-
.attr('height', d => d.height)
|
| 342 |
-
.attr('fill', d => d.color)
|
| 343 |
-
.attr('rx', 2)
|
| 344 |
-
.classed('dimmed', d => hoveredExperiment && d.experiment !== hoveredExperiment)
|
| 345 |
-
.on('mouseenter', (event, d) => {
|
| 346 |
-
hoveredExperiment = d.experiment;
|
| 347 |
-
updateAll();
|
| 348 |
-
tooltip
|
| 349 |
-
.style('opacity', 1)
|
| 350 |
-
.html(`
|
| 351 |
-
<div><strong>${d.experiment}</strong></div>
|
| 352 |
-
<div style="margin-top: 4px;">${metric.label}</div>
|
| 353 |
-
<div style="margin-top: 4px;"><strong>Mean:</strong> ${metric.format(d.mean)}</div>
|
| 354 |
-
<div><strong>Std:</strong> ${metric.format(d.std)}</div>
|
| 355 |
-
`);
|
| 356 |
-
})
|
| 357 |
-
.on('mousemove', (event) => {
|
| 358 |
-
const [mx, my] = d3.pointer(event, card);
|
| 359 |
-
tooltip.style('transform', `translate(${mx + 10}px, ${my + 10}px)`);
|
| 360 |
-
})
|
| 361 |
-
.on('mouseleave', () => {
|
| 362 |
-
hoveredExperiment = null;
|
| 363 |
-
updateAll();
|
| 364 |
-
tooltip.style('opacity', 0).style('transform', 'translate(-9999px, -9999px)');
|
| 365 |
-
});
|
| 366 |
-
|
| 367 |
-
// Error bars
|
| 368 |
-
gErrorBars.selectAll('line')
|
| 369 |
-
.data(bars)
|
| 370 |
-
.join('line')
|
| 371 |
-
.attr('x1', d => d.x + d.width / 2)
|
| 372 |
-
.attr('x2', d => d.x + d.width / 2)
|
| 373 |
-
.attr('y1', d => y(d.mean + d.std))
|
| 374 |
-
.attr('y2', d => y(Math.max(0, d.mean - d.std)))
|
| 375 |
-
.attr('stroke', '#666')
|
| 376 |
-
.attr('stroke-width', 1.5)
|
| 377 |
-
.attr('opacity', 0.6);
|
| 378 |
-
|
| 379 |
-
// Error bar caps
|
| 380 |
-
gErrorBars.selectAll('.cap-top')
|
| 381 |
-
.data(bars)
|
| 382 |
-
.join('line')
|
| 383 |
-
.attr('class', 'cap-top')
|
| 384 |
-
.attr('x1', d => d.x + d.width / 2 - 3)
|
| 385 |
-
.attr('x2', d => d.x + d.width / 2 + 3)
|
| 386 |
-
.attr('y1', d => y(d.mean + d.std))
|
| 387 |
-
.attr('y2', d => y(d.mean + d.std))
|
| 388 |
-
.attr('stroke', '#666')
|
| 389 |
-
.attr('stroke-width', 1.5)
|
| 390 |
-
.attr('opacity', 0.6);
|
| 391 |
-
|
| 392 |
-
gErrorBars.selectAll('.cap-bottom')
|
| 393 |
-
.data(bars)
|
| 394 |
-
.join('line')
|
| 395 |
-
.attr('class', 'cap-bottom')
|
| 396 |
-
.attr('x1', d => d.x + d.width / 2 - 3)
|
| 397 |
-
.attr('x2', d => d.x + d.width / 2 + 3)
|
| 398 |
-
.attr('y1', d => y(Math.max(0, d.mean - d.std)))
|
| 399 |
-
.attr('y2', d => y(Math.max(0, d.mean - d.std)))
|
| 400 |
-
.attr('stroke', '#666')
|
| 401 |
-
.attr('stroke-width', 1.5)
|
| 402 |
-
.attr('opacity', 0.6);
|
| 403 |
-
};
|
| 404 |
-
});
|
| 405 |
-
|
| 406 |
-
// Legend
|
| 407 |
-
const legend = document.createElement('div');
|
| 408 |
-
legend.className = 'legend';
|
| 409 |
-
experiments.forEach(exp => {
|
| 410 |
-
const item = document.createElement('div');
|
| 411 |
-
item.className = 'legend-item';
|
| 412 |
-
item.dataset.experiment = exp;
|
| 413 |
-
item.innerHTML = `
|
| 414 |
-
<div class="legend-swatch" style="background: ${allColors[exp]}"></div>
|
| 415 |
-
<span>${exp}</span>
|
| 416 |
-
`;
|
| 417 |
-
legend.appendChild(item);
|
| 418 |
-
});
|
| 419 |
-
card.appendChild(legend);
|
| 420 |
-
|
| 421 |
-
// Legend interaction
|
| 422 |
-
legend.querySelectorAll('.legend-item').forEach(item => {
|
| 423 |
-
item.addEventListener('mouseenter', () => {
|
| 424 |
-
hoveredExperiment = item.dataset.experiment;
|
| 425 |
-
updateAll();
|
| 426 |
-
});
|
| 427 |
-
item.addEventListener('mouseleave', () => {
|
| 428 |
-
hoveredExperiment = null;
|
| 429 |
-
updateAll();
|
| 430 |
-
});
|
| 431 |
-
});
|
| 432 |
-
|
| 433 |
-
const updateAll = () => {
|
| 434 |
-
gridContainer.querySelectorAll('.subplot').forEach(subplot => {
|
| 435 |
-
if (subplot._render) subplot._render();
|
| 436 |
-
});
|
| 437 |
-
|
| 438 |
-
legend.querySelectorAll('.legend-item').forEach(item => {
|
| 439 |
-
if (hoveredExperiment && item.dataset.experiment !== hoveredExperiment) {
|
| 440 |
-
item.classList.add('dimmed');
|
| 441 |
-
} else {
|
| 442 |
-
item.classList.remove('dimmed');
|
| 443 |
-
}
|
| 444 |
-
});
|
| 445 |
-
};
|
| 446 |
-
|
| 447 |
-
updateAll();
|
| 448 |
-
|
| 449 |
-
if (window.ResizeObserver) {
|
| 450 |
-
const ro = new ResizeObserver(() => updateAll());
|
| 451 |
-
ro.observe(container);
|
| 452 |
-
} else {
|
| 453 |
-
window.addEventListener('resize', updateAll);
|
| 454 |
-
}
|
| 455 |
-
})
|
| 456 |
-
.catch(err => {
|
| 457 |
-
container.innerHTML = `<div style="color: red; padding: 20px;">Error: ${err.message}</div>`;
|
| 458 |
-
});
|
| 459 |
-
};
|
| 460 |
-
|
| 461 |
-
if (document.readyState === 'loading') {
|
| 462 |
-
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 463 |
-
} else {
|
| 464 |
-
ensureD3(bootstrap);
|
| 465 |
-
}
|
| 466 |
-
})();
|
| 467 |
-
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/embeds/d3-evaluation2-clamp.html
DELETED
|
@@ -1,414 +0,0 @@
|
|
| 1 |
-
<div class="d3-eval-grid d3-eval-grid-2"></div>
|
| 2 |
-
<style>
|
| 3 |
-
.d3-eval-grid {
|
| 4 |
-
padding: 2px;
|
| 5 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
.d3-eval-grid .grid-container {
|
| 9 |
-
display: grid;
|
| 10 |
-
grid-template-columns: repeat(2, 1fr);
|
| 11 |
-
gap: 8px;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
@media (max-width: 768px) {
|
| 15 |
-
.d3-eval-grid .grid-container {
|
| 16 |
-
grid-template-columns: 1fr;
|
| 17 |
-
}
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
.d3-eval-grid .subplot {
|
| 21 |
-
padding: 4px;
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
.d3-eval-grid .subplot-title {
|
| 25 |
-
font-size: 12px;
|
| 26 |
-
font-weight: 600;
|
| 27 |
-
color: var(--text-color);
|
| 28 |
-
margin-bottom: 4px;
|
| 29 |
-
text-align: center;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
.d3-eval-grid .axes path,
|
| 34 |
-
.d3-eval-grid .axes line {
|
| 35 |
-
stroke: var(--axis-color);
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
.d3-eval-grid .axes text {
|
| 39 |
-
fill: var(--tick-color);
|
| 40 |
-
font-size: 9px;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
.d3-eval-grid .grid line {
|
| 44 |
-
stroke: var(--grid-color);
|
| 45 |
-
stroke-dasharray: 2,2;
|
| 46 |
-
opacity: 0.5;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
.d3-eval-grid .axis-label {
|
| 50 |
-
fill: var(--text-color);
|
| 51 |
-
font-size: 11px;
|
| 52 |
-
font-weight: 600;
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
.d3-eval-grid .d3-tooltip {
|
| 56 |
-
position: absolute;
|
| 57 |
-
pointer-events: none;
|
| 58 |
-
padding: 8px 10px;
|
| 59 |
-
background: var(--surface-bg);
|
| 60 |
-
border: 1px solid var(--border-color);
|
| 61 |
-
border-radius: 8px;
|
| 62 |
-
font-size: 11px;
|
| 63 |
-
line-height: 1.5;
|
| 64 |
-
box-shadow: 0 4px 24px rgba(0,0,0,.18);
|
| 65 |
-
opacity: 0;
|
| 66 |
-
transition: opacity 0.2s;
|
| 67 |
-
z-index: 1000;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
.d3-eval-grid .bar {
|
| 71 |
-
transition: opacity 0.2s;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
.d3-eval-grid .bar.dimmed {
|
| 75 |
-
opacity: 0.2;
|
| 76 |
-
}
|
| 77 |
-
</style>
|
| 78 |
-
<script>
|
| 79 |
-
(() => {
|
| 80 |
-
const ensureD3 = (cb) => {
|
| 81 |
-
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 82 |
-
let s = document.getElementById('d3-cdn-script');
|
| 83 |
-
if (!s) {
|
| 84 |
-
s = document.createElement('script');
|
| 85 |
-
s.id = 'd3-cdn-script';
|
| 86 |
-
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 87 |
-
document.head.appendChild(s);
|
| 88 |
-
}
|
| 89 |
-
s.addEventListener('load', () => {
|
| 90 |
-
if (window.d3 && typeof window.d3.select === 'function') cb();
|
| 91 |
-
}, { once: true });
|
| 92 |
-
};
|
| 93 |
-
|
| 94 |
-
const bootstrap = () => {
|
| 95 |
-
const scriptEl = document.currentScript;
|
| 96 |
-
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 97 |
-
if (!(container && container.classList && container.classList.contains('d3-eval-grid-2'))) {
|
| 98 |
-
const candidates = Array.from(document.querySelectorAll('.d3-eval-grid-2'))
|
| 99 |
-
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
|
| 100 |
-
container = candidates[candidates.length - 1] || null;
|
| 101 |
-
}
|
| 102 |
-
if (!container) return;
|
| 103 |
-
if (container.dataset) {
|
| 104 |
-
if (container.dataset.mounted === 'true') return;
|
| 105 |
-
container.dataset.mounted = 'true';
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
// Find data attribute
|
| 109 |
-
let mountEl = container;
|
| 110 |
-
while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
|
| 111 |
-
mountEl = mountEl.parentElement;
|
| 112 |
-
}
|
| 113 |
-
let providedData = null;
|
| 114 |
-
try {
|
| 115 |
-
const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
|
| 116 |
-
if (attr && attr.trim()) {
|
| 117 |
-
providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim();
|
| 118 |
-
}
|
| 119 |
-
} catch(_) {}
|
| 120 |
-
|
| 121 |
-
// Check for experiments filter attribute
|
| 122 |
-
let experimentsFilter = null;
|
| 123 |
-
try {
|
| 124 |
-
const expAttr = container.getAttribute('data-experiments');
|
| 125 |
-
if (expAttr) {
|
| 126 |
-
experimentsFilter = JSON.parse(expAttr);
|
| 127 |
-
}
|
| 128 |
-
} catch(_) {}
|
| 129 |
-
|
| 130 |
-
const DEFAULT_JSON = '/data/evaluation_summary.json';
|
| 131 |
-
const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
|
| 132 |
-
|
| 133 |
-
const JSON_PATHS = typeof providedData === 'string'
|
| 134 |
-
? [ensureDataPrefix(providedData)]
|
| 135 |
-
: [
|
| 136 |
-
DEFAULT_JSON,
|
| 137 |
-
'./assets/data/evaluation_summary.json',
|
| 138 |
-
'../assets/data/evaluation_summary.json',
|
| 139 |
-
'../../assets/data/evaluation_summary.json'
|
| 140 |
-
];
|
| 141 |
-
|
| 142 |
-
const fetchFirstAvailable = async (paths) => {
|
| 143 |
-
for (const p of paths) {
|
| 144 |
-
try {
|
| 145 |
-
const r = await fetch(p, { cache: 'no-cache' });
|
| 146 |
-
if (r.ok) return await r.json();
|
| 147 |
-
} catch(_){}
|
| 148 |
-
}
|
| 149 |
-
throw new Error('JSON not found');
|
| 150 |
-
};
|
| 151 |
-
|
| 152 |
-
fetchFirstAvailable(JSON_PATHS)
|
| 153 |
-
.then(rawData => {
|
| 154 |
-
// Chart 2: Add clamping experiments (but reserve space for all)
|
| 155 |
-
const allExperiments = ['Prompt', 'Basic steering', 'Clamping', 'Clamping + Penalty', '2D optimized', '8D optimized'];
|
| 156 |
-
const visibleExperiments = ['Prompt', 'Basic steering', 'Clamping', 'Clamping + Penalty'];
|
| 157 |
-
|
| 158 |
-
// Metrics in 2x4 grid layout (8 metrics)
|
| 159 |
-
const metrics = [
|
| 160 |
-
{ key: 'llm_score_concept', label: 'LLM Concept Score', format: d3.format('.2f') },
|
| 161 |
-
{ key: 'eiffel', label: 'Explicit Concept Presence', format: d3.format('.2f') },
|
| 162 |
-
{ key: 'llm_score_instruction', label: 'LLM Instruction Score', format: d3.format('.2f') },
|
| 163 |
-
{ key: 'minus_log_prob', label: 'Surprise in Original Model', format: d3.format('.2f') },
|
| 164 |
-
{ key: 'llm_score_fluency', label: 'LLM Fluency Score', format: d3.format('.2f') },
|
| 165 |
-
{ key: 'rep3', label: '3-gram Repetition Fraction', format: d3.format('.2f') },
|
| 166 |
-
{ key: 'mean_llm_score', label: 'Mean LLM Score', format: d3.format('.2f') },
|
| 167 |
-
{ key: 'harmonic_llm_score', label: 'Harmonic Mean LLM Score', format: d3.format('.2f') }
|
| 168 |
-
];
|
| 169 |
-
|
| 170 |
-
// Restructure data
|
| 171 |
-
const data = {};
|
| 172 |
-
rawData.forEach(d => {
|
| 173 |
-
if (!data[d.metric]) data[d.metric] = {};
|
| 174 |
-
data[d.metric][d.experiment] = { mean: d.mean, std: d.std };
|
| 175 |
-
});
|
| 176 |
-
|
| 177 |
-
// Color palette - consistent across all charts
|
| 178 |
-
const allColors = {
|
| 179 |
-
'Prompt': '#4c4c4c',
|
| 180 |
-
'Basic steering': '#b2b2b2',
|
| 181 |
-
'Clamping': '#b2b2cc',
|
| 182 |
-
'Clamping + Penalty': '#b2b2e6',
|
| 183 |
-
'2D optimized': '#b2ffb2',
|
| 184 |
-
'8D optimized': '#ffb2ff'
|
| 185 |
-
};
|
| 186 |
-
|
| 187 |
-
const gridContainer = document.createElement('div');
|
| 188 |
-
gridContainer.className = 'grid-container';
|
| 189 |
-
container.appendChild(gridContainer);
|
| 190 |
-
|
| 191 |
-
// Tooltip
|
| 192 |
-
const tooltip = d3.select(container).append('div')
|
| 193 |
-
.attr('class', 'd3-tooltip')
|
| 194 |
-
.style('transform', 'translate(-9999px, -9999px)');
|
| 195 |
-
|
| 196 |
-
let hoveredExperiment = null;
|
| 197 |
-
|
| 198 |
-
// Create each subplot
|
| 199 |
-
metrics.forEach((metric, idx) => {
|
| 200 |
-
const subplot = document.createElement('div');
|
| 201 |
-
subplot.className = 'subplot';
|
| 202 |
-
subplot.dataset.metric = metric.key;
|
| 203 |
-
gridContainer.appendChild(subplot);
|
| 204 |
-
|
| 205 |
-
const title = document.createElement('div');
|
| 206 |
-
title.className = 'subplot-title';
|
| 207 |
-
title.textContent = metric.label;
|
| 208 |
-
subplot.appendChild(title);
|
| 209 |
-
|
| 210 |
-
const svg = d3.select(subplot).append('svg')
|
| 211 |
-
.attr('width', '100%')
|
| 212 |
-
.style('display', 'block');
|
| 213 |
-
|
| 214 |
-
const g = svg.append('g');
|
| 215 |
-
const gGrid = g.append('g').attr('class', 'grid');
|
| 216 |
-
const gBars = g.append('g').attr('class', 'bars');
|
| 217 |
-
const gErrorBars = g.append('g').attr('class', 'error-bars');
|
| 218 |
-
const gAxes = g.append('g').attr('class', 'axes');
|
| 219 |
-
const gLabels = g.append('g').attr('class', 'value-labels');
|
| 220 |
-
|
| 221 |
-
subplot._render = () => {
|
| 222 |
-
const width = subplot.clientWidth || 300;
|
| 223 |
-
const height = Math.max(200, Math.round(width * 0.6));
|
| 224 |
-
const margin = { top: 10, right: 20, bottom: 70, left: 42 };
|
| 225 |
-
const innerWidth = width - margin.left - margin.right;
|
| 226 |
-
const innerHeight = height - margin.top - margin.bottom;
|
| 227 |
-
|
| 228 |
-
svg.attr('height', height);
|
| 229 |
-
g.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 230 |
-
|
| 231 |
-
// Scales - use all experiments for consistent positioning
|
| 232 |
-
const x = d3.scaleBand()
|
| 233 |
-
.domain(allExperiments)
|
| 234 |
-
.range([0, innerWidth])
|
| 235 |
-
.padding(0.2);
|
| 236 |
-
|
| 237 |
-
// Fixed y-axis ranges based on metric type
|
| 238 |
-
const yDomains = {
|
| 239 |
-
'llm_score_concept': [0, 2],
|
| 240 |
-
'llm_score_instruction': [0, 2],
|
| 241 |
-
'llm_score_fluency': [0, 2],
|
| 242 |
-
'mean_llm_score': [0, 2],
|
| 243 |
-
'harmonic_llm_score': [0, 2],
|
| 244 |
-
'eiffel': [0, 1],
|
| 245 |
-
'minus_log_prob': [0, 2],
|
| 246 |
-
'rep3': [0, 0.5]
|
| 247 |
-
};
|
| 248 |
-
|
| 249 |
-
const y = d3.scaleLinear()
|
| 250 |
-
.domain(yDomains[metric.key] || [0, 1])
|
| 251 |
-
.range([innerHeight, 0]);
|
| 252 |
-
|
| 253 |
-
// Grid
|
| 254 |
-
gGrid.selectAll('*').remove();
|
| 255 |
-
gGrid.selectAll('line')
|
| 256 |
-
.data(y.ticks(4))
|
| 257 |
-
.join('line')
|
| 258 |
-
.attr('x1', 0)
|
| 259 |
-
.attr('x2', innerWidth)
|
| 260 |
-
.attr('y1', d => y(d))
|
| 261 |
-
.attr('y2', d => y(d));
|
| 262 |
-
|
| 263 |
-
// Axes
|
| 264 |
-
gAxes.selectAll('*').remove();
|
| 265 |
-
|
| 266 |
-
const xAxis = gAxes.append('g')
|
| 267 |
-
.attr('transform', `translate(0,${innerHeight})`)
|
| 268 |
-
.call(d3.axisBottom(x).tickSize(3));
|
| 269 |
-
|
| 270 |
-
// Only show labels for visible experiments
|
| 271 |
-
xAxis.selectAll('text')
|
| 272 |
-
.attr('transform', 'rotate(-45)')
|
| 273 |
-
.style('text-anchor', 'end')
|
| 274 |
-
.attr('dx', '-0.5em')
|
| 275 |
-
.attr('dy', '0.15em')
|
| 276 |
-
.style('opacity', function() {
|
| 277 |
-
const text = d3.select(this).text();
|
| 278 |
-
return visibleExperiments.includes(text) ? 1 : 0;
|
| 279 |
-
});
|
| 280 |
-
|
| 281 |
-
gAxes.append('g')
|
| 282 |
-
.call(d3.axisLeft(y).ticks(4).tickFormat(metric.format).tickSize(3));
|
| 283 |
-
|
| 284 |
-
// Draw bars (only for visible experiments)
|
| 285 |
-
const bars = [];
|
| 286 |
-
visibleExperiments.forEach(exp => {
|
| 287 |
-
const d = data[metric.key]?.[exp];
|
| 288 |
-
if (d) {
|
| 289 |
-
bars.push({
|
| 290 |
-
experiment: exp,
|
| 291 |
-
mean: d.mean,
|
| 292 |
-
std: d.std,
|
| 293 |
-
color: allColors[exp],
|
| 294 |
-
x: x(exp),
|
| 295 |
-
y: y(d.mean),
|
| 296 |
-
width: x.bandwidth(),
|
| 297 |
-
height: innerHeight - y(d.mean)
|
| 298 |
-
});
|
| 299 |
-
}
|
| 300 |
-
});
|
| 301 |
-
|
| 302 |
-
gBars.selectAll('rect')
|
| 303 |
-
.data(bars)
|
| 304 |
-
.join('rect')
|
| 305 |
-
.attr('class', 'bar')
|
| 306 |
-
.attr('x', d => d.x)
|
| 307 |
-
.attr('y', d => d.y)
|
| 308 |
-
.attr('width', d => d.width)
|
| 309 |
-
.attr('height', d => d.height)
|
| 310 |
-
.attr('fill', d => d.color)
|
| 311 |
-
.attr('rx', 2)
|
| 312 |
-
.classed('dimmed', d => hoveredExperiment && d.experiment !== hoveredExperiment)
|
| 313 |
-
.on('mouseenter', (event, d) => {
|
| 314 |
-
hoveredExperiment = d.experiment;
|
| 315 |
-
|
| 316 |
-
// Show value label on bar
|
| 317 |
-
gLabels.selectAll('text').remove();
|
| 318 |
-
gLabels.append('text')
|
| 319 |
-
.attr('x', d.x + d.width / 2)
|
| 320 |
-
.attr('y', d.y - 5)
|
| 321 |
-
.attr('text-anchor', 'middle')
|
| 322 |
-
.attr('fill', 'var(--text-color)')
|
| 323 |
-
.attr('font-size', '11px')
|
| 324 |
-
.attr('font-weight', '600')
|
| 325 |
-
.text(metric.format(d.mean));
|
| 326 |
-
|
| 327 |
-
updateAll();
|
| 328 |
-
tooltip
|
| 329 |
-
.style('opacity', 1)
|
| 330 |
-
.html(`
|
| 331 |
-
<div><strong>${d.experiment}</strong></div>
|
| 332 |
-
<div style="margin-top: 4px;">${metric.label}</div>
|
| 333 |
-
<div style="margin-top: 4px;"><strong>Mean:</strong> ${metric.format(d.mean)}</div>
|
| 334 |
-
<div><strong>Std:</strong> ${metric.format(d.std)}</div>
|
| 335 |
-
`);
|
| 336 |
-
})
|
| 337 |
-
.on('mousemove', (event) => {
|
| 338 |
-
const [mx, my] = d3.pointer(event, container);
|
| 339 |
-
tooltip.style('transform', `translate(${mx + 10}px, ${my + 10}px)`);
|
| 340 |
-
})
|
| 341 |
-
.on('mouseleave', () => {
|
| 342 |
-
hoveredExperiment = null;
|
| 343 |
-
gLabels.selectAll('text').remove();
|
| 344 |
-
updateAll();
|
| 345 |
-
tooltip.style('opacity', 0).style('transform', 'translate(-9999px, -9999px)');
|
| 346 |
-
});
|
| 347 |
-
|
| 348 |
-
// Error bars
|
| 349 |
-
gErrorBars.selectAll('line')
|
| 350 |
-
.data(bars)
|
| 351 |
-
.join('line')
|
| 352 |
-
.attr('x1', d => d.x + d.width / 2)
|
| 353 |
-
.attr('x2', d => d.x + d.width / 2)
|
| 354 |
-
.attr('y1', d => y(d.mean + d.std))
|
| 355 |
-
.attr('y2', d => y(Math.max(0, d.mean - d.std)))
|
| 356 |
-
.attr('stroke', '#666')
|
| 357 |
-
.attr('stroke-width', 1.5)
|
| 358 |
-
.attr('opacity', 0.6);
|
| 359 |
-
|
| 360 |
-
// Error bar caps
|
| 361 |
-
gErrorBars.selectAll('.cap-top')
|
| 362 |
-
.data(bars)
|
| 363 |
-
.join('line')
|
| 364 |
-
.attr('class', 'cap-top')
|
| 365 |
-
.attr('x1', d => d.x + d.width / 2 - 3)
|
| 366 |
-
.attr('x2', d => d.x + d.width / 2 + 3)
|
| 367 |
-
.attr('y1', d => y(d.mean + d.std))
|
| 368 |
-
.attr('y2', d => y(d.mean + d.std))
|
| 369 |
-
.attr('stroke', '#666')
|
| 370 |
-
.attr('stroke-width', 1.5)
|
| 371 |
-
.attr('opacity', 0.6);
|
| 372 |
-
|
| 373 |
-
gErrorBars.selectAll('.cap-bottom')
|
| 374 |
-
.data(bars)
|
| 375 |
-
.join('line')
|
| 376 |
-
.attr('class', 'cap-bottom')
|
| 377 |
-
.attr('x1', d => d.x + d.width / 2 - 3)
|
| 378 |
-
.attr('x2', d => d.x + d.width / 2 + 3)
|
| 379 |
-
.attr('y1', d => y(Math.max(0, d.mean - d.std)))
|
| 380 |
-
.attr('y2', d => y(Math.max(0, d.mean - d.std)))
|
| 381 |
-
.attr('stroke', '#666')
|
| 382 |
-
.attr('stroke-width', 1.5)
|
| 383 |
-
.attr('opacity', 0.6);
|
| 384 |
-
};
|
| 385 |
-
});
|
| 386 |
-
|
| 387 |
-
const updateAll = () => {
|
| 388 |
-
gridContainer.querySelectorAll('.subplot').forEach(subplot => {
|
| 389 |
-
if (subplot._render) subplot._render();
|
| 390 |
-
});
|
| 391 |
-
|
| 392 |
-
};
|
| 393 |
-
|
| 394 |
-
updateAll();
|
| 395 |
-
|
| 396 |
-
if (window.ResizeObserver) {
|
| 397 |
-
const ro = new ResizeObserver(() => updateAll());
|
| 398 |
-
ro.observe(container);
|
| 399 |
-
} else {
|
| 400 |
-
window.addEventListener('resize', updateAll);
|
| 401 |
-
}
|
| 402 |
-
})
|
| 403 |
-
.catch(err => {
|
| 404 |
-
container.innerHTML = `<div style="color: red; padding: 20px;">Error: ${err.message}</div>`;
|
| 405 |
-
});
|
| 406 |
-
};
|
| 407 |
-
|
| 408 |
-
if (document.readyState === 'loading') {
|
| 409 |
-
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 410 |
-
} else {
|
| 411 |
-
ensureD3(bootstrap);
|
| 412 |
-
}
|
| 413 |
-
})();
|
| 414 |
-
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/embeds/d3-evaluation3-multi.html
DELETED
|
@@ -1,414 +0,0 @@
|
|
| 1 |
-
<div class="d3-eval-grid d3-eval-grid-3"></div>
|
| 2 |
-
<style>
|
| 3 |
-
.d3-eval-grid {
|
| 4 |
-
padding: 2px;
|
| 5 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
.d3-eval-grid .grid-container {
|
| 9 |
-
display: grid;
|
| 10 |
-
grid-template-columns: repeat(2, 1fr);
|
| 11 |
-
gap: 8px;
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
@media (max-width: 768px) {
|
| 15 |
-
.d3-eval-grid .grid-container {
|
| 16 |
-
grid-template-columns: 1fr;
|
| 17 |
-
}
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
.d3-eval-grid .subplot {
|
| 21 |
-
padding: 4px;
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
.d3-eval-grid .subplot-title {
|
| 25 |
-
font-size: 12px;
|
| 26 |
-
font-weight: 600;
|
| 27 |
-
color: var(--text-color);
|
| 28 |
-
margin-bottom: 4px;
|
| 29 |
-
text-align: center;
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
.d3-eval-grid .axes path,
|
| 34 |
-
.d3-eval-grid .axes line {
|
| 35 |
-
stroke: var(--axis-color);
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
.d3-eval-grid .axes text {
|
| 39 |
-
fill: var(--tick-color);
|
| 40 |
-
font-size: 9px;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
.d3-eval-grid .grid line {
|
| 44 |
-
stroke: var(--grid-color);
|
| 45 |
-
stroke-dasharray: 2,2;
|
| 46 |
-
opacity: 0.5;
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
.d3-eval-grid .axis-label {
|
| 50 |
-
fill: var(--text-color);
|
| 51 |
-
font-size: 11px;
|
| 52 |
-
font-weight: 600;
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
.d3-eval-grid .d3-tooltip {
|
| 56 |
-
position: absolute;
|
| 57 |
-
pointer-events: none;
|
| 58 |
-
padding: 8px 10px;
|
| 59 |
-
background: var(--surface-bg);
|
| 60 |
-
border: 1px solid var(--border-color);
|
| 61 |
-
border-radius: 8px;
|
| 62 |
-
font-size: 11px;
|
| 63 |
-
line-height: 1.5;
|
| 64 |
-
box-shadow: 0 4px 24px rgba(0,0,0,.18);
|
| 65 |
-
opacity: 0;
|
| 66 |
-
transition: opacity 0.2s;
|
| 67 |
-
z-index: 1000;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
.d3-eval-grid .bar {
|
| 71 |
-
transition: opacity 0.2s;
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
.d3-eval-grid .bar.dimmed {
|
| 75 |
-
opacity: 0.2;
|
| 76 |
-
}
|
| 77 |
-
</style>
|
| 78 |
-
<script>
|
| 79 |
-
(() => {
|
| 80 |
-
const ensureD3 = (cb) => {
|
| 81 |
-
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 82 |
-
let s = document.getElementById('d3-cdn-script');
|
| 83 |
-
if (!s) {
|
| 84 |
-
s = document.createElement('script');
|
| 85 |
-
s.id = 'd3-cdn-script';
|
| 86 |
-
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 87 |
-
document.head.appendChild(s);
|
| 88 |
-
}
|
| 89 |
-
s.addEventListener('load', () => {
|
| 90 |
-
if (window.d3 && typeof window.d3.select === 'function') cb();
|
| 91 |
-
}, { once: true });
|
| 92 |
-
};
|
| 93 |
-
|
| 94 |
-
const bootstrap = () => {
|
| 95 |
-
const scriptEl = document.currentScript;
|
| 96 |
-
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 97 |
-
if (!(container && container.classList && container.classList.contains('d3-eval-grid-3'))) {
|
| 98 |
-
const candidates = Array.from(document.querySelectorAll('.d3-eval-grid-3'))
|
| 99 |
-
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
|
| 100 |
-
container = candidates[candidates.length - 1] || null;
|
| 101 |
-
}
|
| 102 |
-
if (!container) return;
|
| 103 |
-
if (container.dataset) {
|
| 104 |
-
if (container.dataset.mounted === 'true') return;
|
| 105 |
-
container.dataset.mounted = 'true';
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
// Find data attribute
|
| 109 |
-
let mountEl = container;
|
| 110 |
-
while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
|
| 111 |
-
mountEl = mountEl.parentElement;
|
| 112 |
-
}
|
| 113 |
-
let providedData = null;
|
| 114 |
-
try {
|
| 115 |
-
const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
|
| 116 |
-
if (attr && attr.trim()) {
|
| 117 |
-
providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim();
|
| 118 |
-
}
|
| 119 |
-
} catch(_) {}
|
| 120 |
-
|
| 121 |
-
// Check for experiments filter attribute
|
| 122 |
-
let experimentsFilter = null;
|
| 123 |
-
try {
|
| 124 |
-
const expAttr = container.getAttribute('data-experiments');
|
| 125 |
-
if (expAttr) {
|
| 126 |
-
experimentsFilter = JSON.parse(expAttr);
|
| 127 |
-
}
|
| 128 |
-
} catch(_) {}
|
| 129 |
-
|
| 130 |
-
const DEFAULT_JSON = '/data/evaluation_summary.json';
|
| 131 |
-
const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
|
| 132 |
-
|
| 133 |
-
const JSON_PATHS = typeof providedData === 'string'
|
| 134 |
-
? [ensureDataPrefix(providedData)]
|
| 135 |
-
: [
|
| 136 |
-
DEFAULT_JSON,
|
| 137 |
-
'./assets/data/evaluation_summary.json',
|
| 138 |
-
'../assets/data/evaluation_summary.json',
|
| 139 |
-
'../../assets/data/evaluation_summary.json'
|
| 140 |
-
];
|
| 141 |
-
|
| 142 |
-
const fetchFirstAvailable = async (paths) => {
|
| 143 |
-
for (const p of paths) {
|
| 144 |
-
try {
|
| 145 |
-
const r = await fetch(p, { cache: 'no-cache' });
|
| 146 |
-
if (r.ok) return await r.json();
|
| 147 |
-
} catch(_){}
|
| 148 |
-
}
|
| 149 |
-
throw new Error('JSON not found');
|
| 150 |
-
};
|
| 151 |
-
|
| 152 |
-
fetchFirstAvailable(JSON_PATHS)
|
| 153 |
-
.then(rawData => {
|
| 154 |
-
// Chart 3: All experiments including multi-layer optimization
|
| 155 |
-
const allExperiments = ['Prompt', 'Basic steering', 'Clamping', 'Clamping + Penalty', '2D optimized', '8D optimized'];
|
| 156 |
-
const visibleExperiments = allExperiments;
|
| 157 |
-
|
| 158 |
-
// Metrics in 2x4 grid layout (8 metrics)
|
| 159 |
-
const metrics = [
|
| 160 |
-
{ key: 'llm_score_concept', label: 'LLM Concept Score', format: d3.format('.2f') },
|
| 161 |
-
{ key: 'eiffel', label: 'Explicit Concept Presence', format: d3.format('.2f') },
|
| 162 |
-
{ key: 'llm_score_instruction', label: 'LLM Instruction Score', format: d3.format('.2f') },
|
| 163 |
-
{ key: 'minus_log_prob', label: 'Surprise in Original Model', format: d3.format('.2f') },
|
| 164 |
-
{ key: 'llm_score_fluency', label: 'LLM Fluency Score', format: d3.format('.2f') },
|
| 165 |
-
{ key: 'rep3', label: '3-gram Repetition Fraction', format: d3.format('.2f') },
|
| 166 |
-
{ key: 'mean_llm_score', label: 'Mean LLM Score', format: d3.format('.2f') },
|
| 167 |
-
{ key: 'harmonic_llm_score', label: 'Harmonic Mean LLM Score', format: d3.format('.2f') }
|
| 168 |
-
];
|
| 169 |
-
|
| 170 |
-
// Restructure data
|
| 171 |
-
const data = {};
|
| 172 |
-
rawData.forEach(d => {
|
| 173 |
-
if (!data[d.metric]) data[d.metric] = {};
|
| 174 |
-
data[d.metric][d.experiment] = { mean: d.mean, std: d.std };
|
| 175 |
-
});
|
| 176 |
-
|
| 177 |
-
// Color palette - consistent across all charts
|
| 178 |
-
const allColors = {
|
| 179 |
-
'Prompt': '#4c4c4c',
|
| 180 |
-
'Basic steering': '#b2b2b2',
|
| 181 |
-
'Clamping': '#b2b2cc',
|
| 182 |
-
'Clamping + Penalty': '#b2b2e6',
|
| 183 |
-
'2D optimized': '#b2ffb2',
|
| 184 |
-
'8D optimized': '#ffb2ff'
|
| 185 |
-
};
|
| 186 |
-
|
| 187 |
-
const gridContainer = document.createElement('div');
|
| 188 |
-
gridContainer.className = 'grid-container';
|
| 189 |
-
container.appendChild(gridContainer);
|
| 190 |
-
|
| 191 |
-
// Tooltip
|
| 192 |
-
const tooltip = d3.select(container).append('div')
|
| 193 |
-
.attr('class', 'd3-tooltip')
|
| 194 |
-
.style('transform', 'translate(-9999px, -9999px)');
|
| 195 |
-
|
| 196 |
-
let hoveredExperiment = null;
|
| 197 |
-
|
| 198 |
-
// Create each subplot
|
| 199 |
-
metrics.forEach((metric, idx) => {
|
| 200 |
-
const subplot = document.createElement('div');
|
| 201 |
-
subplot.className = 'subplot';
|
| 202 |
-
subplot.dataset.metric = metric.key;
|
| 203 |
-
gridContainer.appendChild(subplot);
|
| 204 |
-
|
| 205 |
-
const title = document.createElement('div');
|
| 206 |
-
title.className = 'subplot-title';
|
| 207 |
-
title.textContent = metric.label;
|
| 208 |
-
subplot.appendChild(title);
|
| 209 |
-
|
| 210 |
-
const svg = d3.select(subplot).append('svg')
|
| 211 |
-
.attr('width', '100%')
|
| 212 |
-
.style('display', 'block');
|
| 213 |
-
|
| 214 |
-
const g = svg.append('g');
|
| 215 |
-
const gGrid = g.append('g').attr('class', 'grid');
|
| 216 |
-
const gBars = g.append('g').attr('class', 'bars');
|
| 217 |
-
const gErrorBars = g.append('g').attr('class', 'error-bars');
|
| 218 |
-
const gAxes = g.append('g').attr('class', 'axes');
|
| 219 |
-
const gLabels = g.append('g').attr('class', 'value-labels');
|
| 220 |
-
|
| 221 |
-
subplot._render = () => {
|
| 222 |
-
const width = subplot.clientWidth || 300;
|
| 223 |
-
const height = Math.max(200, Math.round(width * 0.6));
|
| 224 |
-
const margin = { top: 10, right: 20, bottom: 70, left: 42 };
|
| 225 |
-
const innerWidth = width - margin.left - margin.right;
|
| 226 |
-
const innerHeight = height - margin.top - margin.bottom;
|
| 227 |
-
|
| 228 |
-
svg.attr('height', height);
|
| 229 |
-
g.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 230 |
-
|
| 231 |
-
// Scales - use all experiments for consistent positioning
|
| 232 |
-
const x = d3.scaleBand()
|
| 233 |
-
.domain(allExperiments)
|
| 234 |
-
.range([0, innerWidth])
|
| 235 |
-
.padding(0.2);
|
| 236 |
-
|
| 237 |
-
// Fixed y-axis ranges based on metric type
|
| 238 |
-
const yDomains = {
|
| 239 |
-
'llm_score_concept': [0, 2],
|
| 240 |
-
'llm_score_instruction': [0, 2],
|
| 241 |
-
'llm_score_fluency': [0, 2],
|
| 242 |
-
'mean_llm_score': [0, 2],
|
| 243 |
-
'harmonic_llm_score': [0, 2],
|
| 244 |
-
'eiffel': [0, 1],
|
| 245 |
-
'minus_log_prob': [0, 2],
|
| 246 |
-
'rep3': [0, 0.5]
|
| 247 |
-
};
|
| 248 |
-
|
| 249 |
-
const y = d3.scaleLinear()
|
| 250 |
-
.domain(yDomains[metric.key] || [0, 1])
|
| 251 |
-
.range([innerHeight, 0]);
|
| 252 |
-
|
| 253 |
-
// Grid
|
| 254 |
-
gGrid.selectAll('*').remove();
|
| 255 |
-
gGrid.selectAll('line')
|
| 256 |
-
.data(y.ticks(4))
|
| 257 |
-
.join('line')
|
| 258 |
-
.attr('x1', 0)
|
| 259 |
-
.attr('x2', innerWidth)
|
| 260 |
-
.attr('y1', d => y(d))
|
| 261 |
-
.attr('y2', d => y(d));
|
| 262 |
-
|
| 263 |
-
// Axes
|
| 264 |
-
gAxes.selectAll('*').remove();
|
| 265 |
-
|
| 266 |
-
const xAxis = gAxes.append('g')
|
| 267 |
-
.attr('transform', `translate(0,${innerHeight})`)
|
| 268 |
-
.call(d3.axisBottom(x).tickSize(3));
|
| 269 |
-
|
| 270 |
-
// Only show labels for visible experiments
|
| 271 |
-
xAxis.selectAll('text')
|
| 272 |
-
.attr('transform', 'rotate(-45)')
|
| 273 |
-
.style('text-anchor', 'end')
|
| 274 |
-
.attr('dx', '-0.5em')
|
| 275 |
-
.attr('dy', '0.15em')
|
| 276 |
-
.style('opacity', function() {
|
| 277 |
-
const text = d3.select(this).text();
|
| 278 |
-
return visibleExperiments.includes(text) ? 1 : 0;
|
| 279 |
-
});
|
| 280 |
-
|
| 281 |
-
gAxes.append('g')
|
| 282 |
-
.call(d3.axisLeft(y).ticks(4).tickFormat(metric.format).tickSize(3));
|
| 283 |
-
|
| 284 |
-
// Draw bars (only for visible experiments)
|
| 285 |
-
const bars = [];
|
| 286 |
-
visibleExperiments.forEach(exp => {
|
| 287 |
-
const d = data[metric.key]?.[exp];
|
| 288 |
-
if (d) {
|
| 289 |
-
bars.push({
|
| 290 |
-
experiment: exp,
|
| 291 |
-
mean: d.mean,
|
| 292 |
-
std: d.std,
|
| 293 |
-
color: allColors[exp],
|
| 294 |
-
x: x(exp),
|
| 295 |
-
y: y(d.mean),
|
| 296 |
-
width: x.bandwidth(),
|
| 297 |
-
height: innerHeight - y(d.mean)
|
| 298 |
-
});
|
| 299 |
-
}
|
| 300 |
-
});
|
| 301 |
-
|
| 302 |
-
gBars.selectAll('rect')
|
| 303 |
-
.data(bars)
|
| 304 |
-
.join('rect')
|
| 305 |
-
.attr('class', 'bar')
|
| 306 |
-
.attr('x', d => d.x)
|
| 307 |
-
.attr('y', d => d.y)
|
| 308 |
-
.attr('width', d => d.width)
|
| 309 |
-
.attr('height', d => d.height)
|
| 310 |
-
.attr('fill', d => d.color)
|
| 311 |
-
.attr('rx', 2)
|
| 312 |
-
.classed('dimmed', d => hoveredExperiment && d.experiment !== hoveredExperiment)
|
| 313 |
-
.on('mouseenter', (event, d) => {
|
| 314 |
-
hoveredExperiment = d.experiment;
|
| 315 |
-
|
| 316 |
-
// Show value label on bar
|
| 317 |
-
gLabels.selectAll('text').remove();
|
| 318 |
-
gLabels.append('text')
|
| 319 |
-
.attr('x', d.x + d.width / 2)
|
| 320 |
-
.attr('y', d.y - 5)
|
| 321 |
-
.attr('text-anchor', 'middle')
|
| 322 |
-
.attr('fill', 'var(--text-color)')
|
| 323 |
-
.attr('font-size', '11px')
|
| 324 |
-
.attr('font-weight', '600')
|
| 325 |
-
.text(metric.format(d.mean));
|
| 326 |
-
|
| 327 |
-
updateAll();
|
| 328 |
-
tooltip
|
| 329 |
-
.style('opacity', 1)
|
| 330 |
-
.html(`
|
| 331 |
-
<div><strong>${d.experiment}</strong></div>
|
| 332 |
-
<div style="margin-top: 4px;">${metric.label}</div>
|
| 333 |
-
<div style="margin-top: 4px;"><strong>Mean:</strong> ${metric.format(d.mean)}</div>
|
| 334 |
-
<div><strong>Std:</strong> ${metric.format(d.std)}</div>
|
| 335 |
-
`);
|
| 336 |
-
})
|
| 337 |
-
.on('mousemove', (event) => {
|
| 338 |
-
const [mx, my] = d3.pointer(event, container);
|
| 339 |
-
tooltip.style('transform', `translate(${mx + 10}px, ${my + 10}px)`);
|
| 340 |
-
})
|
| 341 |
-
.on('mouseleave', () => {
|
| 342 |
-
hoveredExperiment = null;
|
| 343 |
-
gLabels.selectAll('text').remove();
|
| 344 |
-
updateAll();
|
| 345 |
-
tooltip.style('opacity', 0).style('transform', 'translate(-9999px, -9999px)');
|
| 346 |
-
});
|
| 347 |
-
|
| 348 |
-
// Error bars
|
| 349 |
-
gErrorBars.selectAll('line')
|
| 350 |
-
.data(bars)
|
| 351 |
-
.join('line')
|
| 352 |
-
.attr('x1', d => d.x + d.width / 2)
|
| 353 |
-
.attr('x2', d => d.x + d.width / 2)
|
| 354 |
-
.attr('y1', d => y(d.mean + d.std))
|
| 355 |
-
.attr('y2', d => y(Math.max(0, d.mean - d.std)))
|
| 356 |
-
.attr('stroke', '#666')
|
| 357 |
-
.attr('stroke-width', 1.5)
|
| 358 |
-
.attr('opacity', 0.6);
|
| 359 |
-
|
| 360 |
-
// Error bar caps
|
| 361 |
-
gErrorBars.selectAll('.cap-top')
|
| 362 |
-
.data(bars)
|
| 363 |
-
.join('line')
|
| 364 |
-
.attr('class', 'cap-top')
|
| 365 |
-
.attr('x1', d => d.x + d.width / 2 - 3)
|
| 366 |
-
.attr('x2', d => d.x + d.width / 2 + 3)
|
| 367 |
-
.attr('y1', d => y(d.mean + d.std))
|
| 368 |
-
.attr('y2', d => y(d.mean + d.std))
|
| 369 |
-
.attr('stroke', '#666')
|
| 370 |
-
.attr('stroke-width', 1.5)
|
| 371 |
-
.attr('opacity', 0.6);
|
| 372 |
-
|
| 373 |
-
gErrorBars.selectAll('.cap-bottom')
|
| 374 |
-
.data(bars)
|
| 375 |
-
.join('line')
|
| 376 |
-
.attr('class', 'cap-bottom')
|
| 377 |
-
.attr('x1', d => d.x + d.width / 2 - 3)
|
| 378 |
-
.attr('x2', d => d.x + d.width / 2 + 3)
|
| 379 |
-
.attr('y1', d => y(Math.max(0, d.mean - d.std)))
|
| 380 |
-
.attr('y2', d => y(Math.max(0, d.mean - d.std)))
|
| 381 |
-
.attr('stroke', '#666')
|
| 382 |
-
.attr('stroke-width', 1.5)
|
| 383 |
-
.attr('opacity', 0.6);
|
| 384 |
-
};
|
| 385 |
-
});
|
| 386 |
-
|
| 387 |
-
const updateAll = () => {
|
| 388 |
-
gridContainer.querySelectorAll('.subplot').forEach(subplot => {
|
| 389 |
-
if (subplot._render) subplot._render();
|
| 390 |
-
});
|
| 391 |
-
|
| 392 |
-
};
|
| 393 |
-
|
| 394 |
-
updateAll();
|
| 395 |
-
|
| 396 |
-
if (window.ResizeObserver) {
|
| 397 |
-
const ro = new ResizeObserver(() => updateAll());
|
| 398 |
-
ro.observe(container);
|
| 399 |
-
} else {
|
| 400 |
-
window.addEventListener('resize', updateAll);
|
| 401 |
-
}
|
| 402 |
-
})
|
| 403 |
-
.catch(err => {
|
| 404 |
-
container.innerHTML = `<div style="color: red; padding: 20px;">Error: ${err.message}</div>`;
|
| 405 |
-
});
|
| 406 |
-
};
|
| 407 |
-
|
| 408 |
-
if (document.readyState === 'loading') {
|
| 409 |
-
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 410 |
-
} else {
|
| 411 |
-
ensureD3(bootstrap);
|
| 412 |
-
}
|
| 413 |
-
})();
|
| 414 |
-
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/src/content/embeds/d3-first-experiments.html
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
|
| 8 |
.d3-first-experiments .slider-container {
|
| 9 |
margin-bottom: 12px;
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
.d3-first-experiments .slider-label {
|
|
@@ -34,6 +35,10 @@
|
|
| 34 |
var(--primary-color) 100%);
|
| 35 |
outline: none;
|
| 36 |
-webkit-appearance: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
.d3-first-experiments input[type="range"]::-webkit-slider-thumb {
|
|
@@ -61,7 +66,7 @@
|
|
| 61 |
.d3-first-experiments .columns-container {
|
| 62 |
display: grid;
|
| 63 |
grid-template-columns: 1fr 1fr;
|
| 64 |
-
gap:
|
| 65 |
}
|
| 66 |
|
| 67 |
@media (max-width: 768px) {
|
|
@@ -114,11 +119,11 @@
|
|
| 114 |
|
| 115 |
.d3-first-experiments .highlight {
|
| 116 |
font-weight: 700;
|
| 117 |
-
color:
|
| 118 |
}
|
| 119 |
|
| 120 |
.d3-first-experiments .note {
|
| 121 |
-
margin-top:
|
| 122 |
font-size: 11px;
|
| 123 |
color: var(--muted-color);
|
| 124 |
font-style: italic;
|
|
@@ -134,6 +139,28 @@
|
|
| 134 |
font-size: 12px;
|
| 135 |
white-space: pre-wrap;
|
| 136 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
</style>
|
| 138 |
<script>
|
| 139 |
(() => {
|
|
@@ -294,6 +321,11 @@
|
|
| 294 |
const minIntensity = intensities[0];
|
| 295 |
const maxIntensity = intensities[intensities.length - 1];
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
// Create UI
|
| 298 |
container.innerHTML = `
|
| 299 |
<div class="slider-container">
|
|
@@ -307,6 +339,10 @@
|
|
| 307 |
step="0.5"
|
| 308 |
value="${minIntensity}"
|
| 309 |
class="steering-slider">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
</div>
|
| 311 |
<div class="columns-container">
|
| 312 |
<div class="column">
|
|
|
|
| 7 |
|
| 8 |
.d3-first-experiments .slider-container {
|
| 9 |
margin-bottom: 12px;
|
| 10 |
+
position: relative;
|
| 11 |
}
|
| 12 |
|
| 13 |
.d3-first-experiments .slider-label {
|
|
|
|
| 35 |
var(--primary-color) 100%);
|
| 36 |
outline: none;
|
| 37 |
-webkit-appearance: none;
|
| 38 |
+
position: relative;
|
| 39 |
+
z-index: 1;
|
| 40 |
+
margin-top: 20px;
|
| 41 |
+
margin-bottom: 20px;
|
| 42 |
}
|
| 43 |
|
| 44 |
.d3-first-experiments input[type="range"]::-webkit-slider-thumb {
|
|
|
|
| 66 |
.d3-first-experiments .columns-container {
|
| 67 |
display: grid;
|
| 68 |
grid-template-columns: 1fr 1fr;
|
| 69 |
+
gap: 20px;
|
| 70 |
}
|
| 71 |
|
| 72 |
@media (max-width: 768px) {
|
|
|
|
| 119 |
|
| 120 |
.d3-first-experiments .highlight {
|
| 121 |
font-weight: 700;
|
| 122 |
+
color: var(--primary-color);
|
| 123 |
}
|
| 124 |
|
| 125 |
.d3-first-experiments .note {
|
| 126 |
+
margin-top: 20px;
|
| 127 |
font-size: 11px;
|
| 128 |
color: var(--muted-color);
|
| 129 |
font-style: italic;
|
|
|
|
| 139 |
font-size: 12px;
|
| 140 |
white-space: pre-wrap;
|
| 141 |
}
|
| 142 |
+
|
| 143 |
+
.d3-first-experiments .slider-marker {
|
| 144 |
+
position: absolute;
|
| 145 |
+
width: 1px;
|
| 146 |
+
background: rgba(0, 0, 0, 0.25);
|
| 147 |
+
pointer-events: none;
|
| 148 |
+
z-index: 0;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.d3-first-experiments .slider-marker-top {
|
| 152 |
+
top: 40px;
|
| 153 |
+
height: 7px;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.d3-first-experiments .slider-marker-bottom {
|
| 157 |
+
top: 70px;
|
| 158 |
+
height: 7px;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
[data-theme="dark"] .d3-first-experiments .slider-marker {
|
| 162 |
+
background: rgba(255, 255, 255, 0.25);
|
| 163 |
+
}
|
| 164 |
</style>
|
| 165 |
<script>
|
| 166 |
(() => {
|
|
|
|
| 321 |
const minIntensity = intensities[0];
|
| 322 |
const maxIntensity = intensities[intensities.length - 1];
|
| 323 |
|
| 324 |
+
// Calculate marker position for value 6
|
| 325 |
+
const markerValue = 6;
|
| 326 |
+
const markerPosition = ((markerValue - minIntensity) / (maxIntensity - minIntensity)) * 100;
|
| 327 |
+
const showMarker = markerValue >= minIntensity && markerValue <= maxIntensity;
|
| 328 |
+
|
| 329 |
// Create UI
|
| 330 |
container.innerHTML = `
|
| 331 |
<div class="slider-container">
|
|
|
|
| 339 |
step="0.5"
|
| 340 |
value="${minIntensity}"
|
| 341 |
class="steering-slider">
|
| 342 |
+
${showMarker ? `
|
| 343 |
+
<div class="slider-marker slider-marker-top" style="left: ${markerPosition}%;"></div>
|
| 344 |
+
<div class="slider-marker slider-marker-bottom" style="left: ${markerPosition}%;"></div>
|
| 345 |
+
` : ''}
|
| 346 |
</div>
|
| 347 |
<div class="columns-container">
|
| 348 |
<div class="column">
|
app/src/content/embeds/d3-harmonic-mean.html
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Harmonic Mean Charts - Two Side-by-Side Charts
|
| 3 |
+
|
| 4 |
+
Two line charts showing arithmetic and harmonic mean of LLM scores as a function of steering coefficient.
|
| 5 |
+
|
| 6 |
+
Configuration via data-config attribute:
|
| 7 |
+
{
|
| 8 |
+
"dataUrl": "./assets/data/sweep_1d_metrics.csv",
|
| 9 |
+
"xColumn": "alpha"
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
CSV format (with mean/std):
|
| 13 |
+
alpha, arithmetic_mean_mean, arithmetic_mean_std, harmonic_mean_mean, harmonic_mean_std
|
| 14 |
+
|
| 15 |
+
Example usage in MDX:
|
| 16 |
+
<HtmlEmbed
|
| 17 |
+
src="embeds/d3-harmonic-mean.html"
|
| 18 |
+
data="sweep_1d_metrics.csv"
|
| 19 |
+
/>
|
| 20 |
+
-->
|
| 21 |
+
<div class="d3-harmonic-mean"></div>
|
| 22 |
+
<style>
|
| 23 |
+
.d3-harmonic-mean {
|
| 24 |
+
position: relative;
|
| 25 |
+
container-type: inline-size;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/* Grid - 2 columns side by side */
|
| 29 |
+
.d3-harmonic-mean__grid {
|
| 30 |
+
display: grid;
|
| 31 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 32 |
+
gap: 16px;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Container queries - basées sur la largeur du container parent */
|
| 36 |
+
@container (max-width: 600px) {
|
| 37 |
+
.d3-harmonic-mean__grid {
|
| 38 |
+
grid-template-columns: 1fr;
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.chart-cell {
|
| 43 |
+
display: flex;
|
| 44 |
+
flex-direction: column;
|
| 45 |
+
position: relative;
|
| 46 |
+
padding: 12px;
|
| 47 |
+
box-shadow: inset 0 0 0 1px var(--border-color);
|
| 48 |
+
border-radius: 8px;
|
| 49 |
+
background: var(--page-bg);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.chart-cell__title {
|
| 53 |
+
font-size: 13px;
|
| 54 |
+
font-weight: 700;
|
| 55 |
+
color: var(--text-color);
|
| 56 |
+
margin-bottom: 8px;
|
| 57 |
+
padding-bottom: 8px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.chart-cell__body {
|
| 61 |
+
position: relative;
|
| 62 |
+
width: 100%;
|
| 63 |
+
overflow: hidden;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.chart-cell__body svg {
|
| 67 |
+
max-width: 100%;
|
| 68 |
+
height: auto;
|
| 69 |
+
display: block;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.d3-harmonic-mean__legend {
|
| 73 |
+
display: flex;
|
| 74 |
+
gap: 16px;
|
| 75 |
+
margin-top: 16px;
|
| 76 |
+
font-size: 11px;
|
| 77 |
+
color: var(--text-color);
|
| 78 |
+
align-items: center;
|
| 79 |
+
justify-content: center;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.d3-harmonic-mean__legend-item {
|
| 83 |
+
display: flex;
|
| 84 |
+
align-items: center;
|
| 85 |
+
gap: 6px;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.d3-harmonic-mean__legend-line {
|
| 89 |
+
width: 20px;
|
| 90 |
+
height: 2px;
|
| 91 |
+
border-radius: 1px;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.d3-harmonic-mean__legend-band {
|
| 95 |
+
width: 20px;
|
| 96 |
+
height: 12px;
|
| 97 |
+
border-radius: 2px;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* Reset button */
|
| 101 |
+
.chart-cell .reset-button {
|
| 102 |
+
position: absolute;
|
| 103 |
+
top: 12px;
|
| 104 |
+
right: 12px;
|
| 105 |
+
z-index: 10;
|
| 106 |
+
display: none;
|
| 107 |
+
opacity: 0;
|
| 108 |
+
transition: opacity 0.2s ease;
|
| 109 |
+
font-size: 11px;
|
| 110 |
+
padding: 3px 6px;
|
| 111 |
+
border-radius: 4px;
|
| 112 |
+
background: var(--surface-bg);
|
| 113 |
+
color: var(--text-color);
|
| 114 |
+
border: 1px solid var(--border-color);
|
| 115 |
+
cursor: pointer;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Axes */
|
| 119 |
+
.d3-harmonic-mean .axes path {
|
| 120 |
+
display: none;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.d3-harmonic-mean .axes line {
|
| 124 |
+
stroke: var(--axis-color);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.d3-harmonic-mean .axes text {
|
| 128 |
+
fill: var(--tick-color);
|
| 129 |
+
font-size: 10px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.d3-harmonic-mean .axis-label {
|
| 133 |
+
fill: var(--text-color);
|
| 134 |
+
font-size: 10px;
|
| 135 |
+
font-weight: 300;
|
| 136 |
+
opacity: 0.7;
|
| 137 |
+
stroke: var(--page-bg, white);
|
| 138 |
+
stroke-width: 3px;
|
| 139 |
+
paint-order: stroke fill;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.d3-harmonic-mean .grid line {
|
| 143 |
+
stroke: var(--grid-color);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* Lines */
|
| 147 |
+
.d3-harmonic-mean path.main-line {
|
| 148 |
+
fill: none;
|
| 149 |
+
stroke-width: 2;
|
| 150 |
+
transition: opacity 0.2s ease;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* Uncertainty band */
|
| 154 |
+
.d3-harmonic-mean path.uncertainty-band {
|
| 155 |
+
fill: var(--primary-color, #E889AB);
|
| 156 |
+
fill-opacity: 0.2;
|
| 157 |
+
stroke: none;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
/* Tooltip */
|
| 161 |
+
.d3-harmonic-mean .d3-tooltip {
|
| 162 |
+
z-index: 20;
|
| 163 |
+
backdrop-filter: saturate(1.12) blur(8px);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.d3-harmonic-mean .d3-tooltip__inner {
|
| 167 |
+
display: flex;
|
| 168 |
+
flex-direction: column;
|
| 169 |
+
gap: 6px;
|
| 170 |
+
min-width: 200px;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.d3-harmonic-mean .d3-tooltip__inner>div:first-child {
|
| 174 |
+
font-weight: 800;
|
| 175 |
+
letter-spacing: 0.1px;
|
| 176 |
+
margin-bottom: 0;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.d3-harmonic-mean .d3-tooltip__inner>div:nth-child(2) {
|
| 180 |
+
font-size: 11px;
|
| 181 |
+
color: var(--muted-color);
|
| 182 |
+
display: block;
|
| 183 |
+
margin-top: -4px;
|
| 184 |
+
margin-bottom: 2px;
|
| 185 |
+
letter-spacing: 0.1px;
|
| 186 |
+
}
|
| 187 |
+
</style>
|
| 188 |
+
<script>
|
| 189 |
+
(() => {
|
| 190 |
+
const ensureD3 = (cb) => {
|
| 191 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 192 |
+
let s = document.getElementById('d3-cdn-script');
|
| 193 |
+
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
|
| 194 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 195 |
+
s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
|
| 196 |
+
};
|
| 197 |
+
|
| 198 |
+
const bootstrap = () => {
|
| 199 |
+
const scriptEl = document.currentScript;
|
| 200 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 201 |
+
if (!(container && container.classList && container.classList.contains('d3-harmonic-mean'))) {
|
| 202 |
+
const cs = Array.from(document.querySelectorAll('.d3-harmonic-mean')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 203 |
+
container = cs[cs.length - 1] || null;
|
| 204 |
+
}
|
| 205 |
+
if (!container) return;
|
| 206 |
+
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
| 207 |
+
|
| 208 |
+
const d3 = window.d3;
|
| 209 |
+
|
| 210 |
+
// Read config from HtmlEmbed props
|
| 211 |
+
function readEmbedConfig() {
|
| 212 |
+
let mountEl = container;
|
| 213 |
+
while (mountEl && !mountEl.getAttribute?.('data-config')) {
|
| 214 |
+
mountEl = mountEl.parentElement;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
let providedConfig = null;
|
| 218 |
+
try {
|
| 219 |
+
const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
|
| 220 |
+
if (cfg && cfg.trim()) {
|
| 221 |
+
providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
|
| 222 |
+
}
|
| 223 |
+
} catch (e) {
|
| 224 |
+
// Failed to parse data-config
|
| 225 |
+
}
|
| 226 |
+
return providedConfig || {};
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
const embedConfig = readEmbedConfig();
|
| 230 |
+
|
| 231 |
+
// Also check for data-datafiles attribute (used by HtmlEmbed component)
|
| 232 |
+
let providedData = null;
|
| 233 |
+
try {
|
| 234 |
+
let mountEl = container;
|
| 235 |
+
while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
|
| 236 |
+
mountEl = mountEl.parentElement;
|
| 237 |
+
}
|
| 238 |
+
const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
|
| 239 |
+
if (attr && attr.trim()) {
|
| 240 |
+
providedData = attr.trim();
|
| 241 |
+
}
|
| 242 |
+
} catch (e) {}
|
| 243 |
+
|
| 244 |
+
// Metrics configuration - exactly matching the image
|
| 245 |
+
const METRICS = [
|
| 246 |
+
{ key: 'arithmetic_mean', label: 'Arithmetic mean of LLM scores', yAxisLabel: 'Score' },
|
| 247 |
+
{ key: 'harmonic_mean', label: 'Harmonic mean of LLM scores', yAxisLabel: 'Score' }
|
| 248 |
+
];
|
| 249 |
+
|
| 250 |
+
// Determine data URL - try config first, then data attribute, then default
|
| 251 |
+
const dataUrlFromConfig = embedConfig.dataUrl;
|
| 252 |
+
const dataUrlFromAttr = providedData;
|
| 253 |
+
const DEFAULT_CSV = '/data/stats_L15F21576.csv';
|
| 254 |
+
const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
|
| 255 |
+
|
| 256 |
+
const CSV_PATHS = dataUrlFromConfig
|
| 257 |
+
? [dataUrlFromConfig]
|
| 258 |
+
: (dataUrlFromAttr
|
| 259 |
+
? [ensureDataPrefix(dataUrlFromAttr)]
|
| 260 |
+
: [
|
| 261 |
+
DEFAULT_CSV,
|
| 262 |
+
'./assets/data/stats_L15F21576.csv',
|
| 263 |
+
'../assets/data/stats_L15F21576.csv',
|
| 264 |
+
'../../assets/data/stats_L15F21576.csv',
|
| 265 |
+
'./assets/data/sweep_1d_metrics.csv',
|
| 266 |
+
'../assets/data/sweep_1d_metrics.csv'
|
| 267 |
+
]);
|
| 268 |
+
|
| 269 |
+
// Get categorical colors for lines
|
| 270 |
+
const getLineColor = () => {
|
| 271 |
+
try {
|
| 272 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 273 |
+
const colors = window.ColorPalettes.getColors('categorical', 1);
|
| 274 |
+
if (colors && colors.length > 0) return colors[0];
|
| 275 |
+
}
|
| 276 |
+
} catch (_) {}
|
| 277 |
+
return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 278 |
+
};
|
| 279 |
+
|
| 280 |
+
// Configuration
|
| 281 |
+
const CONFIG = {
|
| 282 |
+
csvPaths: CSV_PATHS,
|
| 283 |
+
xColumn: embedConfig.xColumn || 'alpha',
|
| 284 |
+
metrics: METRICS,
|
| 285 |
+
chartHeight: 240,
|
| 286 |
+
margin: { top: 20, right: 20, bottom: 40, left: 50 },
|
| 287 |
+
zoomExtent: [1.0, 8],
|
| 288 |
+
xAxisLabel: embedConfig.xAxisLabel || 'Steering coefficient α',
|
| 289 |
+
lineColor: embedConfig.lineColor || getLineColor()
|
| 290 |
+
};
|
| 291 |
+
|
| 292 |
+
// Create grid
|
| 293 |
+
const grid = document.createElement('div');
|
| 294 |
+
grid.className = 'd3-harmonic-mean__grid';
|
| 295 |
+
container.appendChild(grid);
|
| 296 |
+
|
| 297 |
+
// Create legend container
|
| 298 |
+
const legend = document.createElement('div');
|
| 299 |
+
legend.className = 'd3-harmonic-mean__legend';
|
| 300 |
+
container.appendChild(legend);
|
| 301 |
+
|
| 302 |
+
// Create chart cells
|
| 303 |
+
CONFIG.metrics.forEach((metricConfig, idx) => {
|
| 304 |
+
const cell = document.createElement('div');
|
| 305 |
+
cell.className = 'chart-cell';
|
| 306 |
+
cell.style.zIndex = CONFIG.metrics.length - idx;
|
| 307 |
+
cell.innerHTML = `
|
| 308 |
+
<div class="chart-cell__title">${metricConfig.label}</div>
|
| 309 |
+
<button class="reset-button">Reset</button>
|
| 310 |
+
<div class="chart-cell__body"></div>
|
| 311 |
+
`;
|
| 312 |
+
grid.appendChild(cell);
|
| 313 |
+
});
|
| 314 |
+
|
| 315 |
+
// Data
|
| 316 |
+
let allData = [];
|
| 317 |
+
|
| 318 |
+
// Function to determine smart format based on data values
|
| 319 |
+
function createSmartFormatter(values) {
|
| 320 |
+
if (!values || values.length === 0) return (v) => v;
|
| 321 |
+
|
| 322 |
+
const min = d3.min(values);
|
| 323 |
+
const max = d3.max(values);
|
| 324 |
+
const range = max - min;
|
| 325 |
+
|
| 326 |
+
// Check if all values are effectively integers (within 0.001 tolerance)
|
| 327 |
+
const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001);
|
| 328 |
+
|
| 329 |
+
// Large numbers (billions): format as "X.XXB"
|
| 330 |
+
if (max >= 1e9) {
|
| 331 |
+
return (v) => {
|
| 332 |
+
const billions = v / 1e9;
|
| 333 |
+
return allIntegers && billions === Math.round(billions)
|
| 334 |
+
? d3.format('d')(Math.round(billions)) + 'B'
|
| 335 |
+
: d3.format('.2f')(billions) + 'B';
|
| 336 |
+
};
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
// Millions: format as "X.XXM" or "XM"
|
| 340 |
+
if (max >= 1e6) {
|
| 341 |
+
return (v) => {
|
| 342 |
+
const millions = v / 1e6;
|
| 343 |
+
return allIntegers && millions === Math.round(millions)
|
| 344 |
+
? d3.format('d')(Math.round(millions)) + 'M'
|
| 345 |
+
: d3.format('.2f')(millions) + 'M';
|
| 346 |
+
};
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
// Thousands: format as "X.Xk" or "Xk"
|
| 350 |
+
if (max >= 1000 && range >= 100) {
|
| 351 |
+
return (v) => {
|
| 352 |
+
const thousands = v / 1000;
|
| 353 |
+
return allIntegers && thousands === Math.round(thousands)
|
| 354 |
+
? d3.format('d')(Math.round(thousands)) + 'k'
|
| 355 |
+
: d3.format('.1f')(thousands) + 'k';
|
| 356 |
+
};
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// Regular numbers
|
| 360 |
+
if (allIntegers) {
|
| 361 |
+
return (v) => d3.format('d')(Math.round(v));
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Small decimals: use appropriate precision
|
| 365 |
+
if (range < 1) {
|
| 366 |
+
return (v) => d3.format('.3f')(v);
|
| 367 |
+
} else if (range < 10) {
|
| 368 |
+
return (v) => d3.format('.2f')(v);
|
| 369 |
+
} else {
|
| 370 |
+
return (v) => d3.format('.1f')(v);
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// Init each chart
|
| 375 |
+
function initChart(cellElement, metricConfig) {
|
| 376 |
+
const bodyEl = cellElement.querySelector('.chart-cell__body');
|
| 377 |
+
const resetBtn = cellElement.querySelector('.reset-button');
|
| 378 |
+
|
| 379 |
+
const metricKey = metricConfig.key;
|
| 380 |
+
let hasMoved = false;
|
| 381 |
+
|
| 382 |
+
// Tooltip
|
| 383 |
+
let tip = cellElement.querySelector('.d3-tooltip');
|
| 384 |
+
let tipInner;
|
| 385 |
+
if (!tip) {
|
| 386 |
+
tip = document.createElement('div');
|
| 387 |
+
tip.className = 'd3-tooltip';
|
| 388 |
+
Object.assign(tip.style, {
|
| 389 |
+
position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
|
| 390 |
+
padding: '10px 12px', borderRadius: '12px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
|
| 391 |
+
background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity: '0', transition: 'opacity .12s ease', zIndex: '20'
|
| 392 |
+
});
|
| 393 |
+
tipInner = document.createElement('div');
|
| 394 |
+
tipInner.className = 'd3-tooltip__inner';
|
| 395 |
+
tip.appendChild(tipInner);
|
| 396 |
+
cellElement.appendChild(tip);
|
| 397 |
+
} else {
|
| 398 |
+
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
// Create SVG
|
| 402 |
+
const svg = d3.select(bodyEl).append('svg').attr('width', '100%').style('display', 'block');
|
| 403 |
+
|
| 404 |
+
// Clip path
|
| 405 |
+
const clipId = 'clip-' + Math.random().toString(36).slice(2);
|
| 406 |
+
const clipPath = svg.append('defs').append('clipPath').attr('id', clipId);
|
| 407 |
+
const clipRect = clipPath.append('rect');
|
| 408 |
+
|
| 409 |
+
// Groups
|
| 410 |
+
const g = svg.append('g');
|
| 411 |
+
const gGrid = g.append('g').attr('class', 'grid');
|
| 412 |
+
const gAxes = g.append('g').attr('class', 'axes');
|
| 413 |
+
const gPlot = g.append('g').attr('class', 'plot').attr('clip-path', `url(#${clipId})`);
|
| 414 |
+
const gHover = g.append('g').attr('class', 'hover-layer');
|
| 415 |
+
const overlay = g.append('rect').attr('class', 'overlay').attr('fill', 'none').attr('pointer-events', 'all').style('cursor', 'grab')
|
| 416 |
+
.on('mousedown', function () {
|
| 417 |
+
d3.select(this).style('cursor', 'grabbing');
|
| 418 |
+
tip.style.opacity = '0';
|
| 419 |
+
if (hoverLine) hoverLine.style('display', 'none');
|
| 420 |
+
})
|
| 421 |
+
.on('mouseup', function () { d3.select(this).style('cursor', 'grab'); });
|
| 422 |
+
|
| 423 |
+
// Scales
|
| 424 |
+
const xScale = d3.scaleLinear();
|
| 425 |
+
const yScale = d3.scaleLinear();
|
| 426 |
+
|
| 427 |
+
// Hover state
|
| 428 |
+
let hoverLine = null;
|
| 429 |
+
let dataPoints = [];
|
| 430 |
+
let hideTipTimer = null;
|
| 431 |
+
let hasMeanStd = false;
|
| 432 |
+
|
| 433 |
+
// Formatters (will be set in render())
|
| 434 |
+
let formatX = (v) => v;
|
| 435 |
+
let formatY = (v) => v;
|
| 436 |
+
|
| 437 |
+
// Zoom
|
| 438 |
+
const zoom = d3.zoom().scaleExtent(CONFIG.zoomExtent).on('zoom', zoomed);
|
| 439 |
+
overlay.call(zoom);
|
| 440 |
+
|
| 441 |
+
function zoomed(event) {
|
| 442 |
+
const transform = event.transform;
|
| 443 |
+
hasMoved = transform.k !== 1 || transform.x !== 0 || transform.y !== 0;
|
| 444 |
+
updateResetButton();
|
| 445 |
+
|
| 446 |
+
const newXScale = transform.rescaleX(xScale);
|
| 447 |
+
const newYScale = transform.rescaleY(yScale);
|
| 448 |
+
|
| 449 |
+
const innerWidth = xScale.range()[1];
|
| 450 |
+
|
| 451 |
+
// Update grid
|
| 452 |
+
const gridTicks = newYScale.ticks(5);
|
| 453 |
+
gGrid.selectAll('line').data(gridTicks).join('line')
|
| 454 |
+
.attr('x1', 0).attr('x2', innerWidth)
|
| 455 |
+
.attr('y1', d => newYScale(d)).attr('y2', d => newYScale(d))
|
| 456 |
+
.attr('stroke', 'var(--grid-color)');
|
| 457 |
+
|
| 458 |
+
// Update uncertainty band (if mean/std available)
|
| 459 |
+
if (hasMeanStd && dataPoints[0] && dataPoints[0].yUpper != null) {
|
| 460 |
+
const area = d3.area()
|
| 461 |
+
.x(d => newXScale(d.x))
|
| 462 |
+
.y0(d => newYScale(d.yLower))
|
| 463 |
+
.y1(d => newYScale(d.yUpper))
|
| 464 |
+
.curve(d3.curveMonotoneX);
|
| 465 |
+
|
| 466 |
+
gPlot.selectAll('path.uncertainty-band')
|
| 467 |
+
.attr('d', area(dataPoints));
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
// Update line
|
| 471 |
+
const line = d3.line()
|
| 472 |
+
.x(d => newXScale(d.x))
|
| 473 |
+
.y(d => newYScale(d.y))
|
| 474 |
+
.curve(d3.curveMonotoneX);
|
| 475 |
+
|
| 476 |
+
gPlot.selectAll('path.main-line')
|
| 477 |
+
.attr('d', line(dataPoints));
|
| 478 |
+
|
| 479 |
+
// Update axes
|
| 480 |
+
gAxes.select('.x-axis').call(d3.axisBottom(newXScale).ticks(5).tickSizeOuter(0).tickFormat(formatX));
|
| 481 |
+
gAxes.select('.y-axis').call(d3.axisLeft(newYScale).ticks(5).tickSizeOuter(0).tickFormat(formatY));
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
function updateResetButton() {
|
| 485 |
+
if (hasMoved) {
|
| 486 |
+
resetBtn.style.display = 'block';
|
| 487 |
+
requestAnimationFrame(() => { resetBtn.style.opacity = '1'; });
|
| 488 |
+
} else {
|
| 489 |
+
resetBtn.style.opacity = '0';
|
| 490 |
+
setTimeout(() => { if (!hasMoved) resetBtn.style.display = 'none'; }, 200);
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
function render() {
|
| 495 |
+
const rect = bodyEl.getBoundingClientRect();
|
| 496 |
+
const width = Math.max(1, Math.round(rect.width || 400));
|
| 497 |
+
const height = CONFIG.chartHeight;
|
| 498 |
+
svg.attr('width', width).attr('height', height);
|
| 499 |
+
|
| 500 |
+
const margin = CONFIG.margin;
|
| 501 |
+
const innerWidth = width - margin.left - margin.right;
|
| 502 |
+
const innerHeight = height - margin.top - margin.bottom;
|
| 503 |
+
|
| 504 |
+
g.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 505 |
+
|
| 506 |
+
// Filter and prepare data for this metric
|
| 507 |
+
// Support both mean/std columns and direct value columns
|
| 508 |
+
const meanKey = `${metricKey}_mean`;
|
| 509 |
+
const stdKey = `${metricKey}_std`;
|
| 510 |
+
hasMeanStd = allData.some(d => d[meanKey] != null && d[stdKey] != null);
|
| 511 |
+
|
| 512 |
+
if (hasMeanStd) {
|
| 513 |
+
// Data has mean and std columns
|
| 514 |
+
dataPoints = allData
|
| 515 |
+
.filter(d => d[CONFIG.xColumn] != null && !isNaN(d[CONFIG.xColumn]) &&
|
| 516 |
+
d[meanKey] != null && !isNaN(d[meanKey]) &&
|
| 517 |
+
d[stdKey] != null && !isNaN(d[stdKey]))
|
| 518 |
+
.map(d => ({
|
| 519 |
+
x: +d[CONFIG.xColumn],
|
| 520 |
+
y: +d[meanKey],
|
| 521 |
+
yUpper: +d[meanKey] + +d[stdKey],
|
| 522 |
+
yLower: +d[meanKey] - +d[stdKey]
|
| 523 |
+
}))
|
| 524 |
+
.sort((a, b) => a.x - b.x);
|
| 525 |
+
} else {
|
| 526 |
+
// Data has direct value columns (fallback)
|
| 527 |
+
dataPoints = allData
|
| 528 |
+
.filter(d => d[CONFIG.xColumn] != null && !isNaN(d[CONFIG.xColumn]) && d[metricKey] != null && !isNaN(d[metricKey]))
|
| 529 |
+
.map(d => ({ x: +d[CONFIG.xColumn], y: +d[metricKey] }))
|
| 530 |
+
.sort((a, b) => a.x - b.x);
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
if (!dataPoints.length) {
|
| 534 |
+
return;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
// Auto-compute domains from data
|
| 538 |
+
const xExtent = d3.extent(dataPoints, d => d.x);
|
| 539 |
+
const yExtent = hasMeanStd
|
| 540 |
+
? d3.extent([...dataPoints.map(d => d.yUpper), ...dataPoints.map(d => d.yLower)])
|
| 541 |
+
: d3.extent(dataPoints, d => d.y);
|
| 542 |
+
|
| 543 |
+
// Ensure Y axis never goes below 0
|
| 544 |
+
const yDomain = [Math.max(0, yExtent[0]), yExtent[1]];
|
| 545 |
+
|
| 546 |
+
xScale.domain(xExtent).range([0, innerWidth]);
|
| 547 |
+
yScale.domain(yDomain).range([innerHeight, 0]);
|
| 548 |
+
|
| 549 |
+
// Create smart formatters based on actual data
|
| 550 |
+
const xValues = dataPoints.map(d => d.x);
|
| 551 |
+
const yValues = dataPoints.map(d => d.y);
|
| 552 |
+
formatX = createSmartFormatter(xValues);
|
| 553 |
+
formatY = createSmartFormatter(yValues);
|
| 554 |
+
|
| 555 |
+
// Update clip
|
| 556 |
+
clipRect.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 557 |
+
|
| 558 |
+
// Update overlay
|
| 559 |
+
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 560 |
+
|
| 561 |
+
// Update zoom extent
|
| 562 |
+
zoom.extent([[0, 0], [innerWidth, innerHeight]])
|
| 563 |
+
.translateExtent([[0, 0], [innerWidth, innerHeight]]);
|
| 564 |
+
|
| 565 |
+
// Grid
|
| 566 |
+
gGrid.selectAll('line').data(yScale.ticks(5)).join('line')
|
| 567 |
+
.attr('x1', 0).attr('x2', innerWidth)
|
| 568 |
+
.attr('y1', d => yScale(d)).attr('y2', d => yScale(d))
|
| 569 |
+
.attr('stroke', 'var(--grid-color)');
|
| 570 |
+
|
| 571 |
+
// Axes
|
| 572 |
+
gAxes.selectAll('*').remove();
|
| 573 |
+
gAxes.append('g').attr('class', 'x-axis').attr('transform', `translate(0,${innerHeight})`)
|
| 574 |
+
.call(d3.axisBottom(xScale).ticks(5).tickSizeOuter(0).tickFormat(formatX));
|
| 575 |
+
gAxes.append('g').attr('class', 'y-axis')
|
| 576 |
+
.call(d3.axisLeft(yScale).ticks(5).tickSizeOuter(0).tickFormat(formatY));
|
| 577 |
+
gAxes.selectAll('.domain, .tick line').attr('stroke', 'var(--axis-color)');
|
| 578 |
+
gAxes.selectAll('text').attr('fill', 'var(--tick-color)');
|
| 579 |
+
|
| 580 |
+
// Axis labels
|
| 581 |
+
gAxes.append('text')
|
| 582 |
+
.attr('class', 'axis-label')
|
| 583 |
+
.attr('x', innerWidth / 2)
|
| 584 |
+
.attr('y', innerHeight + 32)
|
| 585 |
+
.attr('text-anchor', 'middle')
|
| 586 |
+
.text(CONFIG.xAxisLabel);
|
| 587 |
+
|
| 588 |
+
gAxes.append('text')
|
| 589 |
+
.attr('class', 'axis-label')
|
| 590 |
+
.attr('transform', 'rotate(-90)')
|
| 591 |
+
.attr('x', -innerHeight / 2)
|
| 592 |
+
.attr('y', -38)
|
| 593 |
+
.attr('text-anchor', 'middle')
|
| 594 |
+
.text(metricConfig.yAxisLabel || 'Value');
|
| 595 |
+
|
| 596 |
+
// Uncertainty band (if mean/std available)
|
| 597 |
+
if (hasMeanStd) {
|
| 598 |
+
const area = d3.area()
|
| 599 |
+
.x(d => xScale(d.x))
|
| 600 |
+
.y0(d => yScale(d.yLower))
|
| 601 |
+
.y1(d => yScale(d.yUpper))
|
| 602 |
+
.curve(d3.curveMonotoneX);
|
| 603 |
+
|
| 604 |
+
gPlot.selectAll('path.uncertainty-band').data([dataPoints]).join('path')
|
| 605 |
+
.attr('class', 'uncertainty-band')
|
| 606 |
+
.attr('d', area);
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// Main line
|
| 610 |
+
const mainLine = d3.line()
|
| 611 |
+
.x(d => xScale(d.x))
|
| 612 |
+
.y(d => yScale(d.y))
|
| 613 |
+
.curve(d3.curveMonotoneX);
|
| 614 |
+
|
| 615 |
+
gPlot.selectAll('path.main-line').data([dataPoints]).join('path')
|
| 616 |
+
.attr('class', 'main-line')
|
| 617 |
+
.attr('fill', 'none')
|
| 618 |
+
.attr('stroke', CONFIG.lineColor)
|
| 619 |
+
.attr('stroke-width', 2)
|
| 620 |
+
.attr('opacity', 0.85)
|
| 621 |
+
.attr('d', mainLine);
|
| 622 |
+
|
| 623 |
+
// Hover
|
| 624 |
+
setupHover(innerWidth, innerHeight);
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
function setupHover(innerWidth, innerHeight) {
|
| 628 |
+
gHover.selectAll('*').remove();
|
| 629 |
+
|
| 630 |
+
hoverLine = gHover.append('line')
|
| 631 |
+
.style('stroke', 'var(--text-color)')
|
| 632 |
+
.attr('stroke-opacity', 0.25)
|
| 633 |
+
.attr('stroke-width', 1)
|
| 634 |
+
.attr('y1', 0)
|
| 635 |
+
.attr('y2', innerHeight)
|
| 636 |
+
.style('display', 'none')
|
| 637 |
+
.attr('pointer-events', 'none');
|
| 638 |
+
|
| 639 |
+
overlay.on('mousemove', function (ev) {
|
| 640 |
+
if (ev.buttons === 0) onHoverMove(ev);
|
| 641 |
+
}).on('mouseleave', onHoverLeave);
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
function onHoverMove(ev) {
|
| 645 |
+
if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; }
|
| 646 |
+
|
| 647 |
+
const [mx, my] = d3.pointer(ev, overlay.node());
|
| 648 |
+
const targetX = xScale.invert(mx);
|
| 649 |
+
|
| 650 |
+
// Find nearest data point
|
| 651 |
+
let nearest = dataPoints[0];
|
| 652 |
+
let minDist = Math.abs(dataPoints[0].x - targetX);
|
| 653 |
+
for (let i = 1; i < dataPoints.length; i++) {
|
| 654 |
+
const dist = Math.abs(dataPoints[i].x - targetX);
|
| 655 |
+
if (dist < minDist) {
|
| 656 |
+
minDist = dist;
|
| 657 |
+
nearest = dataPoints[i];
|
| 658 |
+
}
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
const xpx = xScale(nearest.x);
|
| 662 |
+
hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
|
| 663 |
+
|
| 664 |
+
let html = `<div><strong>${metricConfig.label}</strong></div>`;
|
| 665 |
+
html += `<div>α = ${formatX(nearest.x)}</div>`;
|
| 666 |
+
if (hasMeanStd && nearest.yUpper != null && nearest.yLower != null) {
|
| 667 |
+
html += `<div>Mean: ${formatY(nearest.y)}</div>`;
|
| 668 |
+
html += `<div>± 1 std dev: [${formatY(nearest.yLower)}, ${formatY(nearest.yUpper)}]</div>`;
|
| 669 |
+
} else {
|
| 670 |
+
html += `<div>${metricConfig.yAxisLabel || 'Value'}: ${formatY(nearest.y)}</div>`;
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
tipInner.innerHTML = html;
|
| 674 |
+
const offsetX = 12, offsetY = 12;
|
| 675 |
+
tip.style.opacity = '1';
|
| 676 |
+
tip.style.transform = `translate(${Math.round(mx + offsetX + CONFIG.margin.left)}px, ${Math.round(my + offsetY + CONFIG.margin.top)}px)`;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
function onHoverLeave() {
|
| 680 |
+
hideTipTimer = setTimeout(() => {
|
| 681 |
+
tip.style.opacity = '0';
|
| 682 |
+
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 683 |
+
if (hoverLine) hoverLine.style('display', 'none');
|
| 684 |
+
}, 100);
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
// Reset button
|
| 688 |
+
resetBtn.addEventListener('click', () => {
|
| 689 |
+
overlay.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
|
| 690 |
+
});
|
| 691 |
+
|
| 692 |
+
return { render };
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
// Transform long format CSV to wide format
|
| 696 |
+
function transformLongToWide(longData) {
|
| 697 |
+
// Mapping from CSV quantity names to embed metric keys
|
| 698 |
+
const quantityMap = {
|
| 699 |
+
'arithmetic_mean': 'arithmetic_mean',
|
| 700 |
+
'harmonic_mean': 'harmonic_mean'
|
| 701 |
+
};
|
| 702 |
+
|
| 703 |
+
// Group by steering_intensity
|
| 704 |
+
const grouped = {};
|
| 705 |
+
longData.forEach(row => {
|
| 706 |
+
const intensity = parseFloat(row.steering_intensity);
|
| 707 |
+
if (isNaN(intensity)) return;
|
| 708 |
+
|
| 709 |
+
if (!grouped[intensity]) {
|
| 710 |
+
grouped[intensity] = { alpha: intensity, steering_intensity: intensity };
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
const quantity = row.quantity;
|
| 714 |
+
const statType = row.stat_type;
|
| 715 |
+
const value = parseFloat(row.value);
|
| 716 |
+
|
| 717 |
+
if (isNaN(value)) return;
|
| 718 |
+
|
| 719 |
+
// Map quantity name to metric key
|
| 720 |
+
const metricKey = quantityMap[quantity] || quantity;
|
| 721 |
+
|
| 722 |
+
// Store mean and std
|
| 723 |
+
if (statType === 'mean') {
|
| 724 |
+
grouped[intensity][`${metricKey}_mean`] = value;
|
| 725 |
+
} else if (statType === 'std') {
|
| 726 |
+
grouped[intensity][`${metricKey}_std`] = value;
|
| 727 |
+
}
|
| 728 |
+
});
|
| 729 |
+
|
| 730 |
+
return Object.values(grouped);
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
// Load data
|
| 734 |
+
async function load() {
|
| 735 |
+
try {
|
| 736 |
+
const fetchFirstAvailable = async (paths) => {
|
| 737 |
+
for (const p of paths) {
|
| 738 |
+
try {
|
| 739 |
+
const r = await fetch(p, { cache: 'no-cache' });
|
| 740 |
+
if (r.ok) return await r.text();
|
| 741 |
+
} catch(_){}
|
| 742 |
+
}
|
| 743 |
+
throw new Error('CSV not found at any of the paths: ' + paths.join(', '));
|
| 744 |
+
};
|
| 745 |
+
|
| 746 |
+
const csvText = await fetchFirstAvailable(CONFIG.csvPaths);
|
| 747 |
+
const rawData = d3.csvParse(csvText);
|
| 748 |
+
|
| 749 |
+
// Check if data is in long format (has quantity, stat_type, value columns)
|
| 750 |
+
const isLongFormat = rawData.length > 0 &&
|
| 751 |
+
rawData[0].hasOwnProperty('quantity') &&
|
| 752 |
+
rawData[0].hasOwnProperty('stat_type') &&
|
| 753 |
+
rawData[0].hasOwnProperty('value');
|
| 754 |
+
|
| 755 |
+
if (isLongFormat) {
|
| 756 |
+
allData = transformLongToWide(rawData);
|
| 757 |
+
// Update xColumn to use steering_intensity if available
|
| 758 |
+
if (allData.length > 0 && allData[0].steering_intensity != null) {
|
| 759 |
+
CONFIG.xColumn = 'steering_intensity';
|
| 760 |
+
}
|
| 761 |
+
} else {
|
| 762 |
+
allData = rawData;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
// Init all charts
|
| 766 |
+
const cells = Array.from(grid.querySelectorAll('.chart-cell'));
|
| 767 |
+
const chartInstances = cells.map((cell, idx) => initChart(cell, CONFIG.metrics[idx]));
|
| 768 |
+
|
| 769 |
+
// Render all
|
| 770 |
+
chartInstances.forEach(chart => chart.render());
|
| 771 |
+
|
| 772 |
+
// Update legend (once for the whole group)
|
| 773 |
+
const hasMeanStd = allData.some(d => {
|
| 774 |
+
return CONFIG.metrics.some(m => {
|
| 775 |
+
const meanKey = `${m.key}_mean`;
|
| 776 |
+
const stdKey = `${m.key}_std`;
|
| 777 |
+
return d[meanKey] != null && d[stdKey] != null;
|
| 778 |
+
});
|
| 779 |
+
});
|
| 780 |
+
|
| 781 |
+
if (hasMeanStd) {
|
| 782 |
+
legend.innerHTML = `
|
| 783 |
+
<div class="d3-harmonic-mean__legend-item">
|
| 784 |
+
<div class="d3-harmonic-mean__legend-line" style="background: ${CONFIG.lineColor};"></div>
|
| 785 |
+
<span>Mean</span>
|
| 786 |
+
</div>
|
| 787 |
+
<div class="d3-harmonic-mean__legend-item">
|
| 788 |
+
<div class="d3-harmonic-mean__legend-band" style="background: ${CONFIG.lineColor}; opacity: 0.2;"></div>
|
| 789 |
+
<span>± 1 std dev</span>
|
| 790 |
+
</div>
|
| 791 |
+
`;
|
| 792 |
+
} else {
|
| 793 |
+
legend.innerHTML = `
|
| 794 |
+
<div class="d3-harmonic-mean__legend-item">
|
| 795 |
+
<div class="d3-harmonic-mean__legend-line" style="background: ${CONFIG.lineColor};"></div>
|
| 796 |
+
<span>Mean</span>
|
| 797 |
+
</div>
|
| 798 |
+
`;
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
// Responsive - observe container for resize
|
| 802 |
+
let resizeTimer;
|
| 803 |
+
const handleResize = () => {
|
| 804 |
+
clearTimeout(resizeTimer);
|
| 805 |
+
resizeTimer = setTimeout(() => {
|
| 806 |
+
chartInstances.forEach(chart => chart.render());
|
| 807 |
+
}, 100);
|
| 808 |
+
};
|
| 809 |
+
|
| 810 |
+
const ro = window.ResizeObserver ? new ResizeObserver(handleResize) : null;
|
| 811 |
+
if (ro) {
|
| 812 |
+
ro.observe(container);
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
// Also observe window resize as fallback
|
| 816 |
+
window.addEventListener('resize', handleResize);
|
| 817 |
+
|
| 818 |
+
// Force a re-render after a short delay to ensure proper sizing
|
| 819 |
+
setTimeout(() => {
|
| 820 |
+
chartInstances.forEach(chart => chart.render());
|
| 821 |
+
}, 100);
|
| 822 |
+
|
| 823 |
+
} catch (e) {
|
| 824 |
+
const pre = document.createElement('pre');
|
| 825 |
+
pre.textContent = 'Error loading data: ' + (e && e.message ? e.message : e);
|
| 826 |
+
pre.style.color = 'var(--danger, #b00020)';
|
| 827 |
+
pre.style.fontSize = '12px';
|
| 828 |
+
container.appendChild(pre);
|
| 829 |
+
}
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
load();
|
| 833 |
+
};
|
| 834 |
+
|
| 835 |
+
if (document.readyState === 'loading') {
|
| 836 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 837 |
+
} else {
|
| 838 |
+
ensureD3(bootstrap);
|
| 839 |
+
}
|
| 840 |
+
})();
|
| 841 |
+
</script>
|
| 842 |
+
|
app/src/content/embeds/d3-six-line-chart.html
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Multi-Line Charts Grid
|
| 3 |
+
|
| 4 |
+
A configurable grid of line charts with zoom/pan, smoothing, and hover tooltips.
|
| 5 |
+
|
| 6 |
+
Configuration via data-config attribute:
|
| 7 |
+
{
|
| 8 |
+
"dataUrl": "./assets/data/your_data.csv",
|
| 9 |
+
"charts": [
|
| 10 |
+
{ "title": "Chart 1", "metric": "metric1" },
|
| 11 |
+
{ "title": "Chart 2", "metric": "metric2" },
|
| 12 |
+
...
|
| 13 |
+
],
|
| 14 |
+
"smoothingWindow": 15,
|
| 15 |
+
"smoothingCurve": "monotoneX",
|
| 16 |
+
"gridColumns": 3 // Optional: number of columns (default: 3)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
CSV format: run_name, step, metric1, metric2, ...
|
| 20 |
+
|
| 21 |
+
Example usage in MDX:
|
| 22 |
+
<HtmlEmbed
|
| 23 |
+
src="embeds/d3-six-line-charts.html"
|
| 24 |
+
config={{
|
| 25 |
+
dataUrl: "./assets/data/attention_evals.csv",
|
| 26 |
+
charts: [
|
| 27 |
+
{ title: "HellaSwag", metric: "hellaswag" },
|
| 28 |
+
{ title: "MMLU", metric: "mmlu" },
|
| 29 |
+
{ title: "ARC", metric: "arc" },
|
| 30 |
+
{ title: "PIQA", metric: "piqa" },
|
| 31 |
+
{ title: "OpenBookQA", metric: "openbookqa" },
|
| 32 |
+
{ title: "WinoGrande", metric: "winogrande" }
|
| 33 |
+
],
|
| 34 |
+
smoothingWindow: 15
|
| 35 |
+
}}
|
| 36 |
+
/>
|
| 37 |
+
-->
|
| 38 |
+
<div class="d3-multi-charts"></div>
|
| 39 |
+
<style>
|
| 40 |
+
.d3-multi-charts {
|
| 41 |
+
position: relative;
|
| 42 |
+
container-type: inline-size;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* Legend header */
|
| 46 |
+
.d3-multi-charts__header {
|
| 47 |
+
display: flex;
|
| 48 |
+
align-items: center;
|
| 49 |
+
justify-content: center;
|
| 50 |
+
margin-bottom: 16px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.d3-multi-charts__header .legend-bottom {
|
| 54 |
+
display: flex;
|
| 55 |
+
flex-direction: column;
|
| 56 |
+
align-items: center;
|
| 57 |
+
gap: 6px;
|
| 58 |
+
font-size: 12px;
|
| 59 |
+
color: var(--text-color);
|
| 60 |
+
max-width: 80%;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.d3-multi-charts__header .legend-bottom .legend-title {
|
| 64 |
+
font-size: 12px;
|
| 65 |
+
font-weight: 700;
|
| 66 |
+
color: var(--text-color);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.d3-multi-charts__header .legend-bottom .items {
|
| 70 |
+
display: flex;
|
| 71 |
+
flex-wrap: wrap;
|
| 72 |
+
gap: 8px 14px;
|
| 73 |
+
justify-content: center;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.d3-multi-charts__header .legend-bottom .item {
|
| 77 |
+
display: inline-flex;
|
| 78 |
+
align-items: center;
|
| 79 |
+
gap: 6px;
|
| 80 |
+
white-space: nowrap;
|
| 81 |
+
cursor: pointer;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.d3-multi-charts__header .legend-bottom .swatch {
|
| 85 |
+
width: 14px;
|
| 86 |
+
height: 14px;
|
| 87 |
+
border-radius: 3px;
|
| 88 |
+
border: 1px solid var(--border-color);
|
| 89 |
+
display: inline-block;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/* Grid */
|
| 93 |
+
.d3-multi-charts__grid {
|
| 94 |
+
display: grid;
|
| 95 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 96 |
+
gap: 12px;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* Container queries - basées sur la largeur du container parent, pas de la viewport */
|
| 100 |
+
@container (max-width: 900px) {
|
| 101 |
+
.d3-multi-charts__grid {
|
| 102 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
@container (max-width: 600px) {
|
| 107 |
+
.d3-multi-charts__grid {
|
| 108 |
+
grid-template-columns: 1fr;
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.chart-cell {
|
| 113 |
+
display: flex;
|
| 114 |
+
flex-direction: column;
|
| 115 |
+
position: relative;
|
| 116 |
+
padding: 12px;
|
| 117 |
+
box-shadow: inset 0 0 0 1px var(--border-color);
|
| 118 |
+
border-radius: 8px;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.chart-cell__title {
|
| 122 |
+
font-size: 13px;
|
| 123 |
+
font-weight: 700;
|
| 124 |
+
color: var(--text-color);
|
| 125 |
+
margin-bottom: 8px;
|
| 126 |
+
padding-bottom: 8px;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.chart-cell__body {
|
| 130 |
+
position: relative;
|
| 131 |
+
width: 100%;
|
| 132 |
+
overflow: hidden;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.chart-cell__body svg {
|
| 136 |
+
max-width: 100%;
|
| 137 |
+
height: auto;
|
| 138 |
+
display: block;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* Reset button */
|
| 142 |
+
.chart-cell .reset-button {
|
| 143 |
+
position: absolute;
|
| 144 |
+
top: 12px;
|
| 145 |
+
right: 12px;
|
| 146 |
+
z-index: 10;
|
| 147 |
+
display: none;
|
| 148 |
+
opacity: 0;
|
| 149 |
+
transition: opacity 0.2s ease;
|
| 150 |
+
font-size: 11px;
|
| 151 |
+
padding: 3px 6px;
|
| 152 |
+
border-radius: 4px;
|
| 153 |
+
background: var(--surface-bg);
|
| 154 |
+
color: var(--text-color);
|
| 155 |
+
border: 1px solid var(--border-color);
|
| 156 |
+
cursor: pointer;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
/* Axes */
|
| 160 |
+
.d3-multi-charts .axes path {
|
| 161 |
+
display: none;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.d3-multi-charts .axes line {
|
| 165 |
+
stroke: var(--axis-color);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.d3-multi-charts .axes text {
|
| 169 |
+
fill: var(--tick-color);
|
| 170 |
+
font-size: 10px;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.d3-multi-charts .axis-label {
|
| 174 |
+
fill: var(--text-color);
|
| 175 |
+
font-size: 10px;
|
| 176 |
+
font-weight: 300;
|
| 177 |
+
opacity: 0.7;
|
| 178 |
+
stroke: var(--page-bg, white);
|
| 179 |
+
stroke-width: 3px;
|
| 180 |
+
paint-order: stroke fill;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.d3-multi-charts .grid line {
|
| 184 |
+
stroke: var(--grid-color);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/* Lines */
|
| 188 |
+
.d3-multi-charts path.main-line {
|
| 189 |
+
transition: opacity 0.2s ease;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.d3-multi-charts path.ghost-line {
|
| 193 |
+
transition: opacity 0.6s ease;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/* Ghosting on hover */
|
| 197 |
+
.d3-multi-charts.hovering path.main-line.ghost {
|
| 198 |
+
opacity: .25;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.d3-multi-charts.hovering path.ghost-line.ghost {
|
| 202 |
+
opacity: .05;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.d3-multi-charts.hovering .legend-bottom .item.ghost {
|
| 206 |
+
opacity: .35;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/* Tooltip */
|
| 210 |
+
.d3-multi-charts .d3-tooltip {
|
| 211 |
+
z-index: 20;
|
| 212 |
+
backdrop-filter: saturate(1.12) blur(8px);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.d3-multi-charts .d3-tooltip__inner {
|
| 216 |
+
display: flex;
|
| 217 |
+
flex-direction: column;
|
| 218 |
+
gap: 6px;
|
| 219 |
+
min-width: 200px;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.d3-multi-charts .d3-tooltip__inner>div:first-child {
|
| 223 |
+
font-weight: 800;
|
| 224 |
+
letter-spacing: 0.1px;
|
| 225 |
+
margin-bottom: 0;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.d3-multi-charts .d3-tooltip__inner>div:nth-child(2) {
|
| 229 |
+
font-size: 11px;
|
| 230 |
+
color: var(--muted-color);
|
| 231 |
+
display: block;
|
| 232 |
+
margin-top: -4px;
|
| 233 |
+
margin-bottom: 2px;
|
| 234 |
+
letter-spacing: 0.1px;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.d3-multi-charts .d3-tooltip__inner>div:nth-child(n+3) {
|
| 238 |
+
padding-top: 6px;
|
| 239 |
+
border-top: 1px solid var(--border-color);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.d3-multi-charts .d3-tooltip__color-dot {
|
| 243 |
+
display: inline-block;
|
| 244 |
+
width: 12px;
|
| 245 |
+
height: 12px;
|
| 246 |
+
border-radius: 3px;
|
| 247 |
+
border: 1px solid var(--border-color);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/* Trackio footer removed */
|
| 251 |
+
</style>
|
| 252 |
+
<script>
|
| 253 |
+
(() => {
|
| 254 |
+
const ensureD3 = (cb) => {
|
| 255 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 256 |
+
let s = document.getElementById('d3-cdn-script');
|
| 257 |
+
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
|
| 258 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 259 |
+
s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
|
| 260 |
+
};
|
| 261 |
+
|
| 262 |
+
const bootstrap = () => {
|
| 263 |
+
const scriptEl = document.currentScript;
|
| 264 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 265 |
+
if (!(container && container.classList && container.classList.contains('d3-multi-charts'))) {
|
| 266 |
+
const cs = Array.from(document.querySelectorAll('.d3-multi-charts')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 267 |
+
container = cs[cs.length - 1] || null;
|
| 268 |
+
}
|
| 269 |
+
if (!container) return;
|
| 270 |
+
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
| 271 |
+
|
| 272 |
+
const d3 = window.d3;
|
| 273 |
+
|
| 274 |
+
// Read config from HtmlEmbed props
|
| 275 |
+
function readEmbedConfig() {
|
| 276 |
+
let mountEl = container;
|
| 277 |
+
while (mountEl && !mountEl.getAttribute?.('data-config')) {
|
| 278 |
+
mountEl = mountEl.parentElement;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
let providedConfig = null;
|
| 282 |
+
try {
|
| 283 |
+
const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
|
| 284 |
+
if (cfg && cfg.trim()) {
|
| 285 |
+
providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
|
| 286 |
+
}
|
| 287 |
+
} catch (e) {
|
| 288 |
+
// Failed to parse data-config
|
| 289 |
+
}
|
| 290 |
+
return providedConfig || {};
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
const embedConfig = readEmbedConfig();
|
| 294 |
+
|
| 295 |
+
// Configuration
|
| 296 |
+
const CONFIG = {
|
| 297 |
+
dataUrl: embedConfig.dataUrl || './assets/data/attention_evals.csv',
|
| 298 |
+
charts: embedConfig.charts || [],
|
| 299 |
+
smoothing: embedConfig.smoothing !== undefined ? embedConfig.smoothing : false,
|
| 300 |
+
smoothingWindow: embedConfig.smoothingWindow || 15,
|
| 301 |
+
smoothingCurve: embedConfig.smoothingCurve || 'monotoneX',
|
| 302 |
+
gridColumns: embedConfig.gridColumns || 3,
|
| 303 |
+
chartHeight: 240,
|
| 304 |
+
margin: { top: 20, right: 20, bottom: 40, left: 50 },
|
| 305 |
+
zoomExtent: [1.0, 8],
|
| 306 |
+
xAxisLabel: embedConfig.xAxisLabel || 'Consumed tokens',
|
| 307 |
+
yAxisLabel: embedConfig.yAxisLabel || 'Value',
|
| 308 |
+
xColumn: embedConfig.xColumn || 'tokens',
|
| 309 |
+
runColumn: embedConfig.runColumn || 'run_name'
|
| 310 |
+
};
|
| 311 |
+
|
| 312 |
+
if (!CONFIG.charts.length) {
|
| 313 |
+
container.innerHTML = '<p style="color: var(--danger); font-size: 12px;">Error: No charts configured</p>';
|
| 314 |
+
return;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
// Create legend header
|
| 318 |
+
const header = document.createElement('div');
|
| 319 |
+
header.className = 'd3-multi-charts__header';
|
| 320 |
+
header.innerHTML = `
|
| 321 |
+
<div class="legend-bottom">
|
| 322 |
+
<div class="legend-title">Legend</div>
|
| 323 |
+
<div class="items"></div>
|
| 324 |
+
</div>
|
| 325 |
+
`;
|
| 326 |
+
container.appendChild(header);
|
| 327 |
+
|
| 328 |
+
// Create grid
|
| 329 |
+
const grid = document.createElement('div');
|
| 330 |
+
grid.className = 'd3-multi-charts__grid';
|
| 331 |
+
container.appendChild(grid);
|
| 332 |
+
|
| 333 |
+
// Trackio footer removed
|
| 334 |
+
|
| 335 |
+
// Create chart cells
|
| 336 |
+
CONFIG.charts.forEach((chartConfig, idx) => {
|
| 337 |
+
const cell = document.createElement('div');
|
| 338 |
+
cell.className = 'chart-cell';
|
| 339 |
+
cell.style.zIndex = CONFIG.charts.length - idx; // Stacking order
|
| 340 |
+
cell.innerHTML = `
|
| 341 |
+
<div class="chart-cell__title">${chartConfig.title}</div>
|
| 342 |
+
<button class="reset-button">Reset</button>
|
| 343 |
+
<div class="chart-cell__body"></div>
|
| 344 |
+
`;
|
| 345 |
+
grid.appendChild(cell);
|
| 346 |
+
});
|
| 347 |
+
|
| 348 |
+
// Data
|
| 349 |
+
let allData = [];
|
| 350 |
+
let runList = [];
|
| 351 |
+
let runColorMap = {};
|
| 352 |
+
|
| 353 |
+
// Smoothing
|
| 354 |
+
const getCurve = (smooth) => {
|
| 355 |
+
if (!smooth) return d3.curveLinear;
|
| 356 |
+
switch (CONFIG.smoothingCurve) {
|
| 357 |
+
case 'catmullRom': return d3.curveCatmullRom.alpha(0.5);
|
| 358 |
+
case 'monotoneX': return d3.curveMonotoneX;
|
| 359 |
+
case 'basis': return d3.curveBasis;
|
| 360 |
+
default: return d3.curveLinear;
|
| 361 |
+
}
|
| 362 |
+
};
|
| 363 |
+
|
| 364 |
+
function movingAverage(values, windowSize) {
|
| 365 |
+
if (!Array.isArray(values) || values.length === 0 || windowSize <= 1) return values;
|
| 366 |
+
const half = Math.floor(windowSize / 2);
|
| 367 |
+
const out = new Array(values.length);
|
| 368 |
+
for (let i = 0; i < values.length; i++) {
|
| 369 |
+
let sum = 0; let count = 0;
|
| 370 |
+
const start = Math.max(0, i - half);
|
| 371 |
+
const end = Math.min(values.length - 1, i + half);
|
| 372 |
+
for (let j = start; j <= end; j++) { if (!Number.isNaN(values[j].value)) { sum += values[j].value; count++; } }
|
| 373 |
+
const avg = count ? (sum / count) : values[i].value;
|
| 374 |
+
out[i] = { step: values[i].step, value: avg };
|
| 375 |
+
}
|
| 376 |
+
return out;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
function applySmoothing(values, smooth) {
|
| 380 |
+
if (!smooth) return values;
|
| 381 |
+
return movingAverage(values, CONFIG.smoothingWindow);
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
// Function to determine smart format based on data values
|
| 385 |
+
function createSmartFormatter(values) {
|
| 386 |
+
if (!values || values.length === 0) return (v) => v;
|
| 387 |
+
|
| 388 |
+
const min = d3.min(values);
|
| 389 |
+
const max = d3.max(values);
|
| 390 |
+
const range = max - min;
|
| 391 |
+
|
| 392 |
+
// Check if all values are effectively integers (within 0.001 tolerance)
|
| 393 |
+
const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001);
|
| 394 |
+
|
| 395 |
+
// Large numbers (billions): format as "X.XXB"
|
| 396 |
+
if (max >= 1e9) {
|
| 397 |
+
return (v) => {
|
| 398 |
+
const billions = v / 1e9;
|
| 399 |
+
return allIntegers && billions === Math.round(billions)
|
| 400 |
+
? d3.format('d')(Math.round(billions)) + 'B'
|
| 401 |
+
: d3.format('.2f')(billions) + 'B';
|
| 402 |
+
};
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
// Millions: format as "X.XXM" or "XM"
|
| 406 |
+
if (max >= 1e6) {
|
| 407 |
+
return (v) => {
|
| 408 |
+
const millions = v / 1e6;
|
| 409 |
+
return allIntegers && millions === Math.round(millions)
|
| 410 |
+
? d3.format('d')(Math.round(millions)) + 'M'
|
| 411 |
+
: d3.format('.2f')(millions) + 'M';
|
| 412 |
+
};
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
// Thousands: format as "X.Xk" or "Xk"
|
| 416 |
+
if (max >= 1000 && range >= 100) {
|
| 417 |
+
return (v) => {
|
| 418 |
+
const thousands = v / 1000;
|
| 419 |
+
return allIntegers && thousands === Math.round(thousands)
|
| 420 |
+
? d3.format('d')(Math.round(thousands)) + 'k'
|
| 421 |
+
: d3.format('.1f')(thousands) + 'k';
|
| 422 |
+
};
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
// Regular numbers
|
| 426 |
+
if (allIntegers) {
|
| 427 |
+
return (v) => d3.format('d')(Math.round(v));
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
// Small decimals: use appropriate precision
|
| 431 |
+
if (range < 1) {
|
| 432 |
+
return (v) => d3.format('.3f')(v);
|
| 433 |
+
} else if (range < 10) {
|
| 434 |
+
return (v) => d3.format('.2f')(v);
|
| 435 |
+
} else {
|
| 436 |
+
return (v) => d3.format('.1f')(v);
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
// Colors
|
| 441 |
+
const getRunColors = (n) => {
|
| 442 |
+
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch (_) { }
|
| 443 |
+
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 444 |
+
return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', '#9B59B6', '#16A085', ...(d3.schemeTableau10 || [])].slice(0, n);
|
| 445 |
+
};
|
| 446 |
+
|
| 447 |
+
// Init each chart
|
| 448 |
+
function initChart(cellElement, chartConfig) {
|
| 449 |
+
const bodyEl = cellElement.querySelector('.chart-cell__body');
|
| 450 |
+
const resetBtn = cellElement.querySelector('.reset-button');
|
| 451 |
+
|
| 452 |
+
const metric = chartConfig.metric;
|
| 453 |
+
let smoothEnabled = CONFIG.smoothing;
|
| 454 |
+
let hasMoved = false;
|
| 455 |
+
|
| 456 |
+
// Tooltip
|
| 457 |
+
let tip = cellElement.querySelector('.d3-tooltip');
|
| 458 |
+
let tipInner;
|
| 459 |
+
if (!tip) {
|
| 460 |
+
tip = document.createElement('div');
|
| 461 |
+
tip.className = 'd3-tooltip';
|
| 462 |
+
Object.assign(tip.style, {
|
| 463 |
+
position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
|
| 464 |
+
padding: '10px 12px', borderRadius: '12px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
|
| 465 |
+
background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity: '0', transition: 'opacity .12s ease', zIndex: '20'
|
| 466 |
+
});
|
| 467 |
+
tipInner = document.createElement('div');
|
| 468 |
+
tipInner.className = 'd3-tooltip__inner';
|
| 469 |
+
tip.appendChild(tipInner);
|
| 470 |
+
cellElement.appendChild(tip);
|
| 471 |
+
} else {
|
| 472 |
+
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
// Create SVG
|
| 476 |
+
const svg = d3.select(bodyEl).append('svg').attr('width', '100%').style('display', 'block');
|
| 477 |
+
|
| 478 |
+
// Clip path
|
| 479 |
+
const clipId = 'clip-' + Math.random().toString(36).slice(2);
|
| 480 |
+
const clipPath = svg.append('defs').append('clipPath').attr('id', clipId);
|
| 481 |
+
const clipRect = clipPath.append('rect');
|
| 482 |
+
|
| 483 |
+
// Groups
|
| 484 |
+
const g = svg.append('g');
|
| 485 |
+
const gGrid = g.append('g').attr('class', 'grid');
|
| 486 |
+
const gAxes = g.append('g').attr('class', 'axes');
|
| 487 |
+
const gPlot = g.append('g').attr('class', 'plot').attr('clip-path', `url(#${clipId})`);
|
| 488 |
+
const gHover = g.append('g').attr('class', 'hover-layer');
|
| 489 |
+
const overlay = g.append('rect').attr('class', 'overlay').attr('fill', 'none').attr('pointer-events', 'all').style('cursor', 'grab')
|
| 490 |
+
.on('mousedown', function () {
|
| 491 |
+
d3.select(this).style('cursor', 'grabbing');
|
| 492 |
+
tip.style.opacity = '0';
|
| 493 |
+
if (hoverLine) hoverLine.style('display', 'none');
|
| 494 |
+
})
|
| 495 |
+
.on('mouseup', function () { d3.select(this).style('cursor', 'grab'); });
|
| 496 |
+
|
| 497 |
+
// Scales
|
| 498 |
+
const xScale = d3.scaleLinear();
|
| 499 |
+
const yScale = d3.scaleLinear();
|
| 500 |
+
|
| 501 |
+
// Hover state
|
| 502 |
+
let hoverLine = null;
|
| 503 |
+
let steps = [];
|
| 504 |
+
let hideTipTimer = null;
|
| 505 |
+
|
| 506 |
+
// Formatters (will be set in render())
|
| 507 |
+
let formatStep = (v) => v;
|
| 508 |
+
let formatValue = (v) => v;
|
| 509 |
+
|
| 510 |
+
// Zoom
|
| 511 |
+
const zoom = d3.zoom().scaleExtent(CONFIG.zoomExtent).on('zoom', zoomed);
|
| 512 |
+
overlay.call(zoom);
|
| 513 |
+
|
| 514 |
+
function zoomed(event) {
|
| 515 |
+
const transform = event.transform;
|
| 516 |
+
hasMoved = transform.k !== 1 || transform.x !== 0 || transform.y !== 0;
|
| 517 |
+
updateResetButton();
|
| 518 |
+
|
| 519 |
+
const newXScale = transform.rescaleX(xScale);
|
| 520 |
+
const newYScale = transform.rescaleY(yScale);
|
| 521 |
+
|
| 522 |
+
const innerWidth = xScale.range()[1];
|
| 523 |
+
|
| 524 |
+
// Update grid
|
| 525 |
+
const gridTicks = newYScale.ticks(5);
|
| 526 |
+
gGrid.selectAll('line').data(gridTicks).join('line')
|
| 527 |
+
.attr('x1', 0).attr('x2', innerWidth)
|
| 528 |
+
.attr('y1', d => newYScale(d)).attr('y2', d => newYScale(d))
|
| 529 |
+
.attr('stroke', 'var(--grid-color)');
|
| 530 |
+
|
| 531 |
+
// Update lines
|
| 532 |
+
const line = d3.line()
|
| 533 |
+
.x(d => newXScale(d.step))
|
| 534 |
+
.y(d => newYScale(d.value))
|
| 535 |
+
.curve(getCurve(smoothEnabled));
|
| 536 |
+
|
| 537 |
+
gPlot.selectAll('path.ghost-line')
|
| 538 |
+
.attr('d', d => {
|
| 539 |
+
const rawLine = d3.line().x(d => newXScale(d.step)).y(d => newYScale(d.value)).curve(d3.curveLinear);
|
| 540 |
+
return rawLine(d.values);
|
| 541 |
+
});
|
| 542 |
+
|
| 543 |
+
gPlot.selectAll('path.main-line')
|
| 544 |
+
.attr('d', d => line(applySmoothing(d.values, smoothEnabled)));
|
| 545 |
+
|
| 546 |
+
// Update axes
|
| 547 |
+
gAxes.select('.x-axis').call(d3.axisBottom(newXScale).ticks(5).tickSizeOuter(0).tickFormat(formatStep));
|
| 548 |
+
gAxes.select('.y-axis').call(d3.axisLeft(newYScale).ticks(5).tickSizeOuter(0).tickFormat(formatValue));
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
function updateResetButton() {
|
| 552 |
+
if (hasMoved) {
|
| 553 |
+
resetBtn.style.display = 'block';
|
| 554 |
+
requestAnimationFrame(() => { resetBtn.style.opacity = '1'; });
|
| 555 |
+
} else {
|
| 556 |
+
resetBtn.style.opacity = '0';
|
| 557 |
+
setTimeout(() => { if (!hasMoved) resetBtn.style.display = 'none'; }, 200);
|
| 558 |
+
}
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
function render() {
|
| 562 |
+
const rect = bodyEl.getBoundingClientRect();
|
| 563 |
+
const width = Math.max(1, Math.round(rect.width || 400));
|
| 564 |
+
const height = CONFIG.chartHeight;
|
| 565 |
+
svg.attr('width', width).attr('height', height);
|
| 566 |
+
|
| 567 |
+
const margin = CONFIG.margin;
|
| 568 |
+
const innerWidth = width - margin.left - margin.right;
|
| 569 |
+
const innerHeight = height - margin.top - margin.bottom;
|
| 570 |
+
|
| 571 |
+
g.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 572 |
+
|
| 573 |
+
// Filter data for this metric
|
| 574 |
+
const metricData = allData.filter(d => d[metric] != null && !isNaN(d[metric]));
|
| 575 |
+
|
| 576 |
+
if (!metricData.length) {
|
| 577 |
+
return;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
// Auto-compute domains from data
|
| 581 |
+
const stepExtent = d3.extent(metricData, d => d.step);
|
| 582 |
+
const valueExtent = d3.extent(metricData, d => d[metric]);
|
| 583 |
+
|
| 584 |
+
xScale.domain(stepExtent).range([0, innerWidth]);
|
| 585 |
+
yScale.domain(valueExtent).range([innerHeight, 0]);
|
| 586 |
+
|
| 587 |
+
// Create smart formatters based on actual data
|
| 588 |
+
const stepValues = metricData.map(d => d.step);
|
| 589 |
+
const metricValues = metricData.map(d => d[metric]);
|
| 590 |
+
formatStep = createSmartFormatter(stepValues);
|
| 591 |
+
formatValue = createSmartFormatter(metricValues);
|
| 592 |
+
|
| 593 |
+
// Update clip
|
| 594 |
+
clipRect.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 595 |
+
|
| 596 |
+
// Update overlay
|
| 597 |
+
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 598 |
+
|
| 599 |
+
// Update zoom extent
|
| 600 |
+
zoom.extent([[0, 0], [innerWidth, innerHeight]])
|
| 601 |
+
.translateExtent([[0, 0], [innerWidth, innerHeight]]);
|
| 602 |
+
|
| 603 |
+
// Grid
|
| 604 |
+
gGrid.selectAll('line').data(yScale.ticks(5)).join('line')
|
| 605 |
+
.attr('x1', 0).attr('x2', innerWidth)
|
| 606 |
+
.attr('y1', d => yScale(d)).attr('y2', d => yScale(d))
|
| 607 |
+
.attr('stroke', 'var(--grid-color)');
|
| 608 |
+
|
| 609 |
+
// Axes
|
| 610 |
+
gAxes.selectAll('*').remove();
|
| 611 |
+
gAxes.append('g').attr('class', 'x-axis').attr('transform', `translate(0,${innerHeight})`)
|
| 612 |
+
.call(d3.axisBottom(xScale).ticks(5).tickSizeOuter(0).tickFormat(formatStep));
|
| 613 |
+
gAxes.append('g').attr('class', 'y-axis')
|
| 614 |
+
.call(d3.axisLeft(yScale).ticks(5).tickSizeOuter(0).tickFormat(formatValue));
|
| 615 |
+
gAxes.selectAll('.domain, .tick line').attr('stroke', 'var(--axis-color)');
|
| 616 |
+
gAxes.selectAll('text').attr('fill', 'var(--tick-color)');
|
| 617 |
+
|
| 618 |
+
// Axis labels
|
| 619 |
+
gAxes.append('text')
|
| 620 |
+
.attr('class', 'axis-label')
|
| 621 |
+
.attr('x', innerWidth / 2)
|
| 622 |
+
.attr('y', innerHeight + 32)
|
| 623 |
+
.attr('text-anchor', 'middle')
|
| 624 |
+
.text(CONFIG.xAxisLabel);
|
| 625 |
+
|
| 626 |
+
gAxes.append('text')
|
| 627 |
+
.attr('class', 'axis-label')
|
| 628 |
+
.attr('transform', 'rotate(-90)')
|
| 629 |
+
.attr('x', -innerHeight / 2)
|
| 630 |
+
.attr('y', -38)
|
| 631 |
+
.attr('text-anchor', 'middle')
|
| 632 |
+
.text(CONFIG.yAxisLabel);
|
| 633 |
+
|
| 634 |
+
// Group data by run
|
| 635 |
+
const dataByRun = {};
|
| 636 |
+
runList.forEach(run => { dataByRun[run] = []; });
|
| 637 |
+
metricData.forEach(d => {
|
| 638 |
+
if (dataByRun[d.run]) dataByRun[d.run].push({ step: d.step, value: d[metric] });
|
| 639 |
+
});
|
| 640 |
+
runList.forEach(run => { dataByRun[run].sort((a, b) => a.step - b.step); });
|
| 641 |
+
|
| 642 |
+
const series = runList.map(run => ({ run, color: runColorMap[run], values: dataByRun[run] })).filter(s => s.values.length > 0);
|
| 643 |
+
|
| 644 |
+
// Ghost lines
|
| 645 |
+
const ghostLine = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value)).curve(d3.curveLinear);
|
| 646 |
+
gPlot.selectAll('path.ghost-line').data(series, d => d.run).join('path')
|
| 647 |
+
.attr('class', 'ghost-line')
|
| 648 |
+
.attr('fill', 'none')
|
| 649 |
+
.attr('stroke', d => d.color)
|
| 650 |
+
.attr('stroke-width', 1.5)
|
| 651 |
+
.attr('opacity', smoothEnabled ? 0.15 : 0)
|
| 652 |
+
.attr('pointer-events', 'none')
|
| 653 |
+
.attr('d', d => ghostLine(d.values));
|
| 654 |
+
|
| 655 |
+
// Main lines
|
| 656 |
+
const mainLine = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value)).curve(getCurve(smoothEnabled));
|
| 657 |
+
gPlot.selectAll('path.main-line').data(series, d => d.run).join('path')
|
| 658 |
+
.attr('class', 'main-line')
|
| 659 |
+
.attr('fill', 'none')
|
| 660 |
+
.attr('stroke', d => d.color)
|
| 661 |
+
.attr('stroke-width', 2)
|
| 662 |
+
.attr('opacity', 0.85)
|
| 663 |
+
.attr('d', d => mainLine(applySmoothing(d.values, smoothEnabled)));
|
| 664 |
+
|
| 665 |
+
// Hover
|
| 666 |
+
setupHover(series, innerWidth, innerHeight);
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
function setupHover(series, innerWidth, innerHeight) {
|
| 670 |
+
gHover.selectAll('*').remove();
|
| 671 |
+
|
| 672 |
+
hoverLine = gHover.append('line')
|
| 673 |
+
.style('stroke', 'var(--text-color)')
|
| 674 |
+
.attr('stroke-opacity', 0.25)
|
| 675 |
+
.attr('stroke-width', 1)
|
| 676 |
+
.attr('y1', 0)
|
| 677 |
+
.attr('y2', innerHeight)
|
| 678 |
+
.style('display', 'none')
|
| 679 |
+
.attr('pointer-events', 'none');
|
| 680 |
+
|
| 681 |
+
const stepSet = new Set();
|
| 682 |
+
series.forEach(s => s.values.forEach(v => stepSet.add(v.step)));
|
| 683 |
+
steps = Array.from(stepSet).sort((a, b) => a - b);
|
| 684 |
+
|
| 685 |
+
overlay.on('mousemove', function (ev) {
|
| 686 |
+
if (ev.buttons === 0) onHoverMove(ev, series);
|
| 687 |
+
}).on('mouseleave', onHoverLeave);
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
function onHoverMove(ev, series) {
|
| 691 |
+
if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; }
|
| 692 |
+
|
| 693 |
+
const [mx, my] = d3.pointer(ev, overlay.node());
|
| 694 |
+
const targetStep = xScale.invert(mx);
|
| 695 |
+
const nearest = steps.reduce((best, t) => Math.abs(t - targetStep) < Math.abs(best - targetStep) ? t : best, steps[0]);
|
| 696 |
+
|
| 697 |
+
const xpx = xScale(nearest);
|
| 698 |
+
hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
|
| 699 |
+
|
| 700 |
+
let html = `<div><strong>${chartConfig.title}</strong></div>`;
|
| 701 |
+
html += `<div>${formatStep(nearest)}</div>`;
|
| 702 |
+
|
| 703 |
+
const entries = series.map(s => {
|
| 704 |
+
const values = s.values;
|
| 705 |
+
let before = null, after = null;
|
| 706 |
+
for (let i = 0; i < values.length; i++) {
|
| 707 |
+
if (values[i].step <= nearest) before = values[i];
|
| 708 |
+
if (values[i].step >= nearest && !after) { after = values[i]; break; }
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
let interpolatedValue = null;
|
| 712 |
+
if (before && after && before.step !== after.step) {
|
| 713 |
+
const t = (nearest - before.step) / (after.step - before.step);
|
| 714 |
+
interpolatedValue = before.value + t * (after.value - before.value);
|
| 715 |
+
} else if (before && before.step === nearest) {
|
| 716 |
+
interpolatedValue = before.value;
|
| 717 |
+
} else if (after && after.step === nearest) {
|
| 718 |
+
interpolatedValue = after.value;
|
| 719 |
+
} else if (before) {
|
| 720 |
+
interpolatedValue = before.value;
|
| 721 |
+
} else if (after) {
|
| 722 |
+
interpolatedValue = after.value;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
return { run: s.run, color: s.color, value: interpolatedValue };
|
| 726 |
+
}).filter(e => e.value != null);
|
| 727 |
+
|
| 728 |
+
entries.sort((a, b) => b.value - a.value);
|
| 729 |
+
|
| 730 |
+
entries.forEach(e => {
|
| 731 |
+
html += `<div style="display:flex;align-items:center;gap:8px;"><span class="d3-tooltip__color-dot" style="background:${e.color}"></span><span>${e.run}</span><span style="margin-left:auto;font-weight:normal;">${e.value.toFixed(4)}</span></div>`;
|
| 732 |
+
});
|
| 733 |
+
|
| 734 |
+
tipInner.innerHTML = html;
|
| 735 |
+
const offsetX = 12, offsetY = 12;
|
| 736 |
+
tip.style.opacity = '1';
|
| 737 |
+
tip.style.transform = `translate(${Math.round(mx + offsetX + CONFIG.margin.left)}px, ${Math.round(my + offsetY + CONFIG.margin.top)}px)`;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
function onHoverLeave() {
|
| 741 |
+
hideTipTimer = setTimeout(() => {
|
| 742 |
+
tip.style.opacity = '0';
|
| 743 |
+
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 744 |
+
if (hoverLine) hoverLine.style('display', 'none');
|
| 745 |
+
}, 100);
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
// Reset button
|
| 749 |
+
resetBtn.addEventListener('click', () => {
|
| 750 |
+
overlay.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
|
| 751 |
+
});
|
| 752 |
+
|
| 753 |
+
return { render };
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
// Load data
|
| 757 |
+
async function load() {
|
| 758 |
+
try {
|
| 759 |
+
const response = await fetch(CONFIG.dataUrl, { cache: 'no-cache' });
|
| 760 |
+
if (!response.ok) throw new Error(`Failed to load data: ${response.status} ${response.statusText}`);
|
| 761 |
+
|
| 762 |
+
const csvText = await response.text();
|
| 763 |
+
|
| 764 |
+
// Parse CSV (long format: run_name, metric, tokens, value)
|
| 765 |
+
const rawRows = d3.csvParse(csvText, d => ({
|
| 766 |
+
run: (d[CONFIG.runColumn] || '').trim(),
|
| 767 |
+
metric: (d.metric || '').trim(),
|
| 768 |
+
tokens: +d[CONFIG.xColumn],
|
| 769 |
+
value: +d.value
|
| 770 |
+
}));
|
| 771 |
+
|
| 772 |
+
// Pivot data: group by run + tokens, create columns for each metric
|
| 773 |
+
const pivotMap = new Map();
|
| 774 |
+
rawRows.forEach(row => {
|
| 775 |
+
if (isNaN(row.tokens) || isNaN(row.value)) return;
|
| 776 |
+
|
| 777 |
+
const key = `${row.run}|${row.tokens}`;
|
| 778 |
+
if (!pivotMap.has(key)) {
|
| 779 |
+
pivotMap.set(key, { run: row.run, step: row.tokens });
|
| 780 |
+
}
|
| 781 |
+
const pivotRow = pivotMap.get(key);
|
| 782 |
+
pivotRow[row.metric] = row.value;
|
| 783 |
+
});
|
| 784 |
+
|
| 785 |
+
allData = Array.from(pivotMap.values());
|
| 786 |
+
|
| 787 |
+
runList = Array.from(new Set(allData.map(d => d.run))).sort();
|
| 788 |
+
|
| 789 |
+
const colors = getRunColors(runList.length);
|
| 790 |
+
runList.forEach((run, i) => { runColorMap[run] = colors[i % colors.length]; });
|
| 791 |
+
|
| 792 |
+
// Build legend
|
| 793 |
+
const legendItemsHost = header.querySelector('.legend-bottom .items');
|
| 794 |
+
if (legendItemsHost) {
|
| 795 |
+
legendItemsHost.innerHTML = runList.map(run => {
|
| 796 |
+
const color = runColorMap[run];
|
| 797 |
+
return `<span class="item" data-run="${run}"><span class="swatch" style="background:${color}"></span><span>${run}</span></span>`;
|
| 798 |
+
}).join('');
|
| 799 |
+
|
| 800 |
+
// Add hover interactions
|
| 801 |
+
legendItemsHost.querySelectorAll('.item').forEach(el => {
|
| 802 |
+
el.addEventListener('mouseenter', () => {
|
| 803 |
+
const run = el.getAttribute('data-run');
|
| 804 |
+
container.classList.add('hovering');
|
| 805 |
+
grid.querySelectorAll('path.main-line').forEach(path => {
|
| 806 |
+
const pathRun = d3.select(path).datum()?.run;
|
| 807 |
+
path.classList.toggle('ghost', pathRun !== run);
|
| 808 |
+
});
|
| 809 |
+
grid.querySelectorAll('path.ghost-line').forEach(path => {
|
| 810 |
+
const pathRun = d3.select(path).datum()?.run;
|
| 811 |
+
path.classList.toggle('ghost', pathRun !== run);
|
| 812 |
+
});
|
| 813 |
+
legendItemsHost.querySelectorAll('.item').forEach(it => {
|
| 814 |
+
it.classList.toggle('ghost', it.getAttribute('data-run') !== run);
|
| 815 |
+
});
|
| 816 |
+
});
|
| 817 |
+
|
| 818 |
+
el.addEventListener('mouseleave', () => {
|
| 819 |
+
container.classList.remove('hovering');
|
| 820 |
+
grid.querySelectorAll('path.main-line').forEach(path => path.classList.remove('ghost'));
|
| 821 |
+
grid.querySelectorAll('path.ghost-line').forEach(path => path.classList.remove('ghost'));
|
| 822 |
+
legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));
|
| 823 |
+
});
|
| 824 |
+
});
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
// Init all charts
|
| 828 |
+
const cells = Array.from(grid.querySelectorAll('.chart-cell'));
|
| 829 |
+
const chartInstances = cells.map((cell, idx) => initChart(cell, CONFIG.charts[idx]));
|
| 830 |
+
|
| 831 |
+
// Render all
|
| 832 |
+
chartInstances.forEach(chart => chart.render());
|
| 833 |
+
|
| 834 |
+
// Responsive - observe container for resize
|
| 835 |
+
let resizeTimer;
|
| 836 |
+
const handleResize = () => {
|
| 837 |
+
clearTimeout(resizeTimer);
|
| 838 |
+
resizeTimer = setTimeout(() => {
|
| 839 |
+
chartInstances.forEach(chart => chart.render());
|
| 840 |
+
}, 100);
|
| 841 |
+
};
|
| 842 |
+
|
| 843 |
+
const ro = window.ResizeObserver ? new ResizeObserver(handleResize) : null;
|
| 844 |
+
if (ro) {
|
| 845 |
+
ro.observe(container);
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
// Also observe window resize as fallback
|
| 849 |
+
window.addEventListener('resize', handleResize);
|
| 850 |
+
|
| 851 |
+
// Force a re-render after a short delay to ensure proper sizing
|
| 852 |
+
setTimeout(() => {
|
| 853 |
+
chartInstances.forEach(chart => chart.render());
|
| 854 |
+
}, 100);
|
| 855 |
+
|
| 856 |
+
} catch (e) {
|
| 857 |
+
const pre = document.createElement('pre');
|
| 858 |
+
pre.textContent = 'Error loading data: ' + (e && e.message ? e.message : e);
|
| 859 |
+
pre.style.color = 'var(--danger, #b00020)';
|
| 860 |
+
pre.style.fontSize = '12px';
|
| 861 |
+
container.appendChild(pre);
|
| 862 |
+
}
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
load();
|
| 866 |
+
};
|
| 867 |
+
|
| 868 |
+
if (document.readyState === 'loading') {
|
| 869 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 870 |
+
} else {
|
| 871 |
+
ensureD3(bootstrap);
|
| 872 |
+
}
|
| 873 |
+
})();
|
| 874 |
+
</script>
|
app/src/content/embeds/d3-sweep-1d-metrics.html
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Sweep 1D Metrics - Six Metrics Grid
|
| 3 |
+
|
| 4 |
+
A grid of 6 line charts showing metrics as a function of steering coefficient alpha.
|
| 5 |
+
|
| 6 |
+
Configuration via data-config attribute:
|
| 7 |
+
{
|
| 8 |
+
"dataUrl": "./assets/data/sweep_1d_metrics.csv",
|
| 9 |
+
"xColumn": "alpha",
|
| 10 |
+
"metrics": [
|
| 11 |
+
{ "key": "concept_inclusion", "label": "LLM concept score", "yAxisLabel": "Score" },
|
| 12 |
+
{ "key": "eiffel", "label": "Explicit concept inclusion", "yAxisLabel": "Fraction" },
|
| 13 |
+
{ "key": "instruction_following", "label": "LLM instruction score", "yAxisLabel": "Score" },
|
| 14 |
+
{ "key": "surprise", "label": "Surprise in reference model", "yAxisLabel": "Value" },
|
| 15 |
+
{ "key": "fluency", "label": "LLM fluency score", "yAxisLabel": "Score" },
|
| 16 |
+
{ "key": "repetition", "label": "3-gram repetition", "yAxisLabel": "Fraction" }
|
| 17 |
+
]
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
CSV format (with mean/std):
|
| 21 |
+
alpha, concept_inclusion_mean, concept_inclusion_std, instruction_following_mean, instruction_following_std, ...
|
| 22 |
+
CSV format (simple, fallback):
|
| 23 |
+
alpha, concept_inclusion, instruction_following, fluency, surprise, repetition, eiffel
|
| 24 |
+
|
| 25 |
+
Example usage in MDX:
|
| 26 |
+
<HtmlEmbed
|
| 27 |
+
src="embeds/d3-sweep-1d-metrics.html"
|
| 28 |
+
config={{
|
| 29 |
+
dataUrl: "./assets/data/sweep_1d_metrics.csv"
|
| 30 |
+
}}
|
| 31 |
+
/>
|
| 32 |
+
-->
|
| 33 |
+
<div class="d3-sweep-1d"></div>
|
| 34 |
+
<style>
|
| 35 |
+
.d3-sweep-1d {
|
| 36 |
+
position: relative;
|
| 37 |
+
container-type: inline-size;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Grid - 2 columns x 3 rows */
|
| 41 |
+
.d3-sweep-1d__grid {
|
| 42 |
+
display: grid;
|
| 43 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 44 |
+
gap: 16px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* Container queries - basées sur la largeur du container parent */
|
| 48 |
+
@container (max-width: 600px) {
|
| 49 |
+
.d3-sweep-1d__grid {
|
| 50 |
+
grid-template-columns: 1fr;
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.chart-cell {
|
| 55 |
+
display: flex;
|
| 56 |
+
flex-direction: column;
|
| 57 |
+
position: relative;
|
| 58 |
+
padding: 12px;
|
| 59 |
+
box-shadow: inset 0 0 0 1px var(--border-color);
|
| 60 |
+
border-radius: 8px;
|
| 61 |
+
background: var(--page-bg);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.chart-cell__title {
|
| 65 |
+
font-size: 13px;
|
| 66 |
+
font-weight: 700;
|
| 67 |
+
color: var(--text-color);
|
| 68 |
+
margin-bottom: 8px;
|
| 69 |
+
padding-bottom: 8px;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.chart-cell__body {
|
| 73 |
+
position: relative;
|
| 74 |
+
width: 100%;
|
| 75 |
+
overflow: hidden;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.chart-cell__body svg {
|
| 79 |
+
max-width: 100%;
|
| 80 |
+
height: auto;
|
| 81 |
+
display: block;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.d3-sweep-1d__legend {
|
| 85 |
+
display: flex;
|
| 86 |
+
gap: 16px;
|
| 87 |
+
margin-top: 16px;
|
| 88 |
+
font-size: 11px;
|
| 89 |
+
color: var(--text-color);
|
| 90 |
+
align-items: center;
|
| 91 |
+
justify-content: center;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.d3-sweep-1d__legend-item {
|
| 95 |
+
display: flex;
|
| 96 |
+
align-items: center;
|
| 97 |
+
gap: 6px;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.d3-sweep-1d__legend-line {
|
| 101 |
+
width: 20px;
|
| 102 |
+
height: 2px;
|
| 103 |
+
border-radius: 1px;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.d3-sweep-1d__legend-band {
|
| 107 |
+
width: 20px;
|
| 108 |
+
height: 12px;
|
| 109 |
+
border-radius: 2px;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/* Reset button */
|
| 113 |
+
.chart-cell .reset-button {
|
| 114 |
+
position: absolute;
|
| 115 |
+
top: 12px;
|
| 116 |
+
right: 12px;
|
| 117 |
+
z-index: 10;
|
| 118 |
+
display: none;
|
| 119 |
+
opacity: 0;
|
| 120 |
+
transition: opacity 0.2s ease;
|
| 121 |
+
font-size: 11px;
|
| 122 |
+
padding: 3px 6px;
|
| 123 |
+
border-radius: 4px;
|
| 124 |
+
background: var(--surface-bg);
|
| 125 |
+
color: var(--text-color);
|
| 126 |
+
border: 1px solid var(--border-color);
|
| 127 |
+
cursor: pointer;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* Axes */
|
| 131 |
+
.d3-sweep-1d .axes path {
|
| 132 |
+
display: none;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.d3-sweep-1d .axes line {
|
| 136 |
+
stroke: var(--axis-color);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.d3-sweep-1d .axes text {
|
| 140 |
+
fill: var(--tick-color);
|
| 141 |
+
font-size: 10px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.d3-sweep-1d .axis-label {
|
| 145 |
+
fill: var(--text-color);
|
| 146 |
+
font-size: 10px;
|
| 147 |
+
font-weight: 300;
|
| 148 |
+
opacity: 0.7;
|
| 149 |
+
stroke: var(--page-bg, white);
|
| 150 |
+
stroke-width: 3px;
|
| 151 |
+
paint-order: stroke fill;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.d3-sweep-1d .grid line {
|
| 155 |
+
stroke: var(--grid-color);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/* Lines */
|
| 159 |
+
.d3-sweep-1d path.main-line {
|
| 160 |
+
fill: none;
|
| 161 |
+
stroke-width: 2;
|
| 162 |
+
transition: opacity 0.2s ease;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/* Uncertainty band */
|
| 166 |
+
.d3-sweep-1d path.uncertainty-band {
|
| 167 |
+
fill: var(--primary-color, #E889AB);
|
| 168 |
+
fill-opacity: 0.2;
|
| 169 |
+
stroke: none;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/* Tooltip */
|
| 173 |
+
.d3-sweep-1d .d3-tooltip {
|
| 174 |
+
z-index: 20;
|
| 175 |
+
backdrop-filter: saturate(1.12) blur(8px);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.d3-sweep-1d .d3-tooltip__inner {
|
| 179 |
+
display: flex;
|
| 180 |
+
flex-direction: column;
|
| 181 |
+
gap: 6px;
|
| 182 |
+
min-width: 200px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.d3-sweep-1d .d3-tooltip__inner>div:first-child {
|
| 186 |
+
font-weight: 800;
|
| 187 |
+
letter-spacing: 0.1px;
|
| 188 |
+
margin-bottom: 0;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.d3-sweep-1d .d3-tooltip__inner>div:nth-child(2) {
|
| 192 |
+
font-size: 11px;
|
| 193 |
+
color: var(--muted-color);
|
| 194 |
+
display: block;
|
| 195 |
+
margin-top: -4px;
|
| 196 |
+
margin-bottom: 2px;
|
| 197 |
+
letter-spacing: 0.1px;
|
| 198 |
+
}
|
| 199 |
+
</style>
|
| 200 |
+
<script>
|
| 201 |
+
(() => {
|
| 202 |
+
const ensureD3 = (cb) => {
|
| 203 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 204 |
+
let s = document.getElementById('d3-cdn-script');
|
| 205 |
+
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
|
| 206 |
+
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
|
| 207 |
+
s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();
|
| 208 |
+
};
|
| 209 |
+
|
| 210 |
+
const bootstrap = () => {
|
| 211 |
+
const scriptEl = document.currentScript;
|
| 212 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 213 |
+
if (!(container && container.classList && container.classList.contains('d3-sweep-1d'))) {
|
| 214 |
+
const cs = Array.from(document.querySelectorAll('.d3-sweep-1d')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));
|
| 215 |
+
container = cs[cs.length - 1] || null;
|
| 216 |
+
}
|
| 217 |
+
if (!container) return;
|
| 218 |
+
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }
|
| 219 |
+
|
| 220 |
+
const d3 = window.d3;
|
| 221 |
+
|
| 222 |
+
// Read config from HtmlEmbed props
|
| 223 |
+
function readEmbedConfig() {
|
| 224 |
+
let mountEl = container;
|
| 225 |
+
while (mountEl && !mountEl.getAttribute?.('data-config')) {
|
| 226 |
+
mountEl = mountEl.parentElement;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
let providedConfig = null;
|
| 230 |
+
try {
|
| 231 |
+
const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
|
| 232 |
+
if (cfg && cfg.trim()) {
|
| 233 |
+
providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
|
| 234 |
+
}
|
| 235 |
+
} catch (e) {
|
| 236 |
+
// Failed to parse data-config
|
| 237 |
+
}
|
| 238 |
+
return providedConfig || {};
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
const embedConfig = readEmbedConfig();
|
| 242 |
+
|
| 243 |
+
// Also check for data-datafiles attribute (used by HtmlEmbed component)
|
| 244 |
+
let providedData = null;
|
| 245 |
+
try {
|
| 246 |
+
let mountEl = container;
|
| 247 |
+
while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
|
| 248 |
+
mountEl = mountEl.parentElement;
|
| 249 |
+
}
|
| 250 |
+
const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
|
| 251 |
+
if (attr && attr.trim()) {
|
| 252 |
+
providedData = attr.trim();
|
| 253 |
+
}
|
| 254 |
+
} catch (e) {}
|
| 255 |
+
|
| 256 |
+
// Default metrics configuration - order matches the image exactly
|
| 257 |
+
const DEFAULT_METRICS = [
|
| 258 |
+
{ key: 'concept_inclusion', label: 'LLM concept score', yAxisLabel: 'Score' },
|
| 259 |
+
{ key: 'eiffel', label: 'Explicit concept inclusion', yAxisLabel: 'Fraction' },
|
| 260 |
+
{ key: 'instruction_following', label: 'LLM instruction score', yAxisLabel: 'Score' },
|
| 261 |
+
{ key: 'surprise', label: 'Surprise in reference model', yAxisLabel: 'Value' },
|
| 262 |
+
{ key: 'fluency', label: 'LLM fluency score', yAxisLabel: 'Score' },
|
| 263 |
+
{ key: 'repetition', label: '3-gram repetition', yAxisLabel: 'Fraction' }
|
| 264 |
+
];
|
| 265 |
+
|
| 266 |
+
// Determine data URL - try config first, then data attribute, then default
|
| 267 |
+
const dataUrlFromConfig = embedConfig.dataUrl;
|
| 268 |
+
const dataUrlFromAttr = providedData;
|
| 269 |
+
const DEFAULT_CSV = '/data/stats_L15F21576.csv';
|
| 270 |
+
const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
|
| 271 |
+
|
| 272 |
+
const CSV_PATHS = dataUrlFromConfig
|
| 273 |
+
? [dataUrlFromConfig]
|
| 274 |
+
: (dataUrlFromAttr
|
| 275 |
+
? [ensureDataPrefix(dataUrlFromAttr)]
|
| 276 |
+
: [
|
| 277 |
+
DEFAULT_CSV,
|
| 278 |
+
'./assets/data/stats_L15F21576.csv',
|
| 279 |
+
'../assets/data/stats_L15F21576.csv',
|
| 280 |
+
'../../assets/data/stats_L15F21576.csv',
|
| 281 |
+
'./assets/data/sweep_1d_metrics.csv',
|
| 282 |
+
'../assets/data/sweep_1d_metrics.csv'
|
| 283 |
+
]);
|
| 284 |
+
|
| 285 |
+
// Get categorical colors for lines
|
| 286 |
+
const getLineColor = () => {
|
| 287 |
+
try {
|
| 288 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 289 |
+
const colors = window.ColorPalettes.getColors('categorical', 1);
|
| 290 |
+
if (colors && colors.length > 0) return colors[0];
|
| 291 |
+
}
|
| 292 |
+
} catch (_) {}
|
| 293 |
+
return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 294 |
+
};
|
| 295 |
+
|
| 296 |
+
// Configuration
|
| 297 |
+
const CONFIG = {
|
| 298 |
+
csvPaths: CSV_PATHS,
|
| 299 |
+
xColumn: embedConfig.xColumn || 'alpha',
|
| 300 |
+
metrics: embedConfig.metrics || DEFAULT_METRICS,
|
| 301 |
+
chartHeight: 240,
|
| 302 |
+
margin: { top: 20, right: 20, bottom: 40, left: 50 },
|
| 303 |
+
zoomExtent: [1.0, 8],
|
| 304 |
+
xAxisLabel: embedConfig.xAxisLabel || 'Steering coefficient α',
|
| 305 |
+
lineColor: embedConfig.lineColor || getLineColor()
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
// Create grid
|
| 309 |
+
const grid = document.createElement('div');
|
| 310 |
+
grid.className = 'd3-sweep-1d__grid';
|
| 311 |
+
container.appendChild(grid);
|
| 312 |
+
|
| 313 |
+
// Create legend container
|
| 314 |
+
const legend = document.createElement('div');
|
| 315 |
+
legend.className = 'd3-sweep-1d__legend';
|
| 316 |
+
container.appendChild(legend);
|
| 317 |
+
|
| 318 |
+
// Create chart cells
|
| 319 |
+
CONFIG.metrics.forEach((metricConfig, idx) => {
|
| 320 |
+
const cell = document.createElement('div');
|
| 321 |
+
cell.className = 'chart-cell';
|
| 322 |
+
cell.style.zIndex = CONFIG.metrics.length - idx;
|
| 323 |
+
cell.innerHTML = `
|
| 324 |
+
<div class="chart-cell__title">${metricConfig.label}</div>
|
| 325 |
+
<button class="reset-button">Reset</button>
|
| 326 |
+
<div class="chart-cell__body"></div>
|
| 327 |
+
`;
|
| 328 |
+
grid.appendChild(cell);
|
| 329 |
+
});
|
| 330 |
+
|
| 331 |
+
// Data
|
| 332 |
+
let allData = [];
|
| 333 |
+
|
| 334 |
+
// Function to determine smart format based on data values
|
| 335 |
+
function createSmartFormatter(values) {
|
| 336 |
+
if (!values || values.length === 0) return (v) => v;
|
| 337 |
+
|
| 338 |
+
const min = d3.min(values);
|
| 339 |
+
const max = d3.max(values);
|
| 340 |
+
const range = max - min;
|
| 341 |
+
|
| 342 |
+
// Check if all values are effectively integers (within 0.001 tolerance)
|
| 343 |
+
const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001);
|
| 344 |
+
|
| 345 |
+
// Large numbers (billions): format as "X.XXB"
|
| 346 |
+
if (max >= 1e9) {
|
| 347 |
+
return (v) => {
|
| 348 |
+
const billions = v / 1e9;
|
| 349 |
+
return allIntegers && billions === Math.round(billions)
|
| 350 |
+
? d3.format('d')(Math.round(billions)) + 'B'
|
| 351 |
+
: d3.format('.2f')(billions) + 'B';
|
| 352 |
+
};
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// Millions: format as "X.XXM" or "XM"
|
| 356 |
+
if (max >= 1e6) {
|
| 357 |
+
return (v) => {
|
| 358 |
+
const millions = v / 1e6;
|
| 359 |
+
return allIntegers && millions === Math.round(millions)
|
| 360 |
+
? d3.format('d')(Math.round(millions)) + 'M'
|
| 361 |
+
: d3.format('.2f')(millions) + 'M';
|
| 362 |
+
};
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
// Thousands: format as "X.Xk" or "Xk"
|
| 366 |
+
if (max >= 1000 && range >= 100) {
|
| 367 |
+
return (v) => {
|
| 368 |
+
const thousands = v / 1000;
|
| 369 |
+
return allIntegers && thousands === Math.round(thousands)
|
| 370 |
+
? d3.format('d')(Math.round(thousands)) + 'k'
|
| 371 |
+
: d3.format('.1f')(thousands) + 'k';
|
| 372 |
+
};
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// Regular numbers
|
| 376 |
+
if (allIntegers) {
|
| 377 |
+
return (v) => d3.format('d')(Math.round(v));
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
// Small decimals: use appropriate precision
|
| 381 |
+
if (range < 1) {
|
| 382 |
+
return (v) => d3.format('.3f')(v);
|
| 383 |
+
} else if (range < 10) {
|
| 384 |
+
return (v) => d3.format('.2f')(v);
|
| 385 |
+
} else {
|
| 386 |
+
return (v) => d3.format('.1f')(v);
|
| 387 |
+
}
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
// Init each chart
|
| 391 |
+
function initChart(cellElement, metricConfig) {
|
| 392 |
+
const bodyEl = cellElement.querySelector('.chart-cell__body');
|
| 393 |
+
const resetBtn = cellElement.querySelector('.reset-button');
|
| 394 |
+
|
| 395 |
+
const metricKey = metricConfig.key;
|
| 396 |
+
let hasMoved = false;
|
| 397 |
+
|
| 398 |
+
// Tooltip
|
| 399 |
+
let tip = cellElement.querySelector('.d3-tooltip');
|
| 400 |
+
let tipInner;
|
| 401 |
+
if (!tip) {
|
| 402 |
+
tip = document.createElement('div');
|
| 403 |
+
tip.className = 'd3-tooltip';
|
| 404 |
+
Object.assign(tip.style, {
|
| 405 |
+
position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none',
|
| 406 |
+
padding: '10px 12px', borderRadius: '12px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)',
|
| 407 |
+
background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity: '0', transition: 'opacity .12s ease', zIndex: '20'
|
| 408 |
+
});
|
| 409 |
+
tipInner = document.createElement('div');
|
| 410 |
+
tipInner.className = 'd3-tooltip__inner';
|
| 411 |
+
tip.appendChild(tipInner);
|
| 412 |
+
cellElement.appendChild(tip);
|
| 413 |
+
} else {
|
| 414 |
+
tipInner = tip.querySelector('.d3-tooltip__inner') || tip;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
// Create SVG
|
| 418 |
+
const svg = d3.select(bodyEl).append('svg').attr('width', '100%').style('display', 'block');
|
| 419 |
+
|
| 420 |
+
// Clip path
|
| 421 |
+
const clipId = 'clip-' + Math.random().toString(36).slice(2);
|
| 422 |
+
const clipPath = svg.append('defs').append('clipPath').attr('id', clipId);
|
| 423 |
+
const clipRect = clipPath.append('rect');
|
| 424 |
+
|
| 425 |
+
// Groups
|
| 426 |
+
const g = svg.append('g');
|
| 427 |
+
const gGrid = g.append('g').attr('class', 'grid');
|
| 428 |
+
const gAxes = g.append('g').attr('class', 'axes');
|
| 429 |
+
const gPlot = g.append('g').attr('class', 'plot').attr('clip-path', `url(#${clipId})`);
|
| 430 |
+
const gHover = g.append('g').attr('class', 'hover-layer');
|
| 431 |
+
const overlay = g.append('rect').attr('class', 'overlay').attr('fill', 'none').attr('pointer-events', 'all').style('cursor', 'grab')
|
| 432 |
+
.on('mousedown', function () {
|
| 433 |
+
d3.select(this).style('cursor', 'grabbing');
|
| 434 |
+
tip.style.opacity = '0';
|
| 435 |
+
if (hoverLine) hoverLine.style('display', 'none');
|
| 436 |
+
})
|
| 437 |
+
.on('mouseup', function () { d3.select(this).style('cursor', 'grab'); });
|
| 438 |
+
|
| 439 |
+
// Scales
|
| 440 |
+
const xScale = d3.scaleLinear();
|
| 441 |
+
const yScale = d3.scaleLinear();
|
| 442 |
+
|
| 443 |
+
// Hover state
|
| 444 |
+
let hoverLine = null;
|
| 445 |
+
let dataPoints = [];
|
| 446 |
+
let hideTipTimer = null;
|
| 447 |
+
let hasMeanStd = false;
|
| 448 |
+
|
| 449 |
+
// Formatters (will be set in render())
|
| 450 |
+
let formatX = (v) => v;
|
| 451 |
+
let formatY = (v) => v;
|
| 452 |
+
|
| 453 |
+
// Zoom
|
| 454 |
+
const zoom = d3.zoom().scaleExtent(CONFIG.zoomExtent).on('zoom', zoomed);
|
| 455 |
+
overlay.call(zoom);
|
| 456 |
+
|
| 457 |
+
function zoomed(event) {
|
| 458 |
+
const transform = event.transform;
|
| 459 |
+
hasMoved = transform.k !== 1 || transform.x !== 0 || transform.y !== 0;
|
| 460 |
+
updateResetButton();
|
| 461 |
+
|
| 462 |
+
const newXScale = transform.rescaleX(xScale);
|
| 463 |
+
const newYScale = transform.rescaleY(yScale);
|
| 464 |
+
|
| 465 |
+
const innerWidth = xScale.range()[1];
|
| 466 |
+
|
| 467 |
+
// Update grid
|
| 468 |
+
const gridTicks = newYScale.ticks(5);
|
| 469 |
+
gGrid.selectAll('line').data(gridTicks).join('line')
|
| 470 |
+
.attr('x1', 0).attr('x2', innerWidth)
|
| 471 |
+
.attr('y1', d => newYScale(d)).attr('y2', d => newYScale(d))
|
| 472 |
+
.attr('stroke', 'var(--grid-color)');
|
| 473 |
+
|
| 474 |
+
// Update uncertainty band (if mean/std available)
|
| 475 |
+
if (hasMeanStd && dataPoints[0] && dataPoints[0].yUpper != null) {
|
| 476 |
+
const area = d3.area()
|
| 477 |
+
.x(d => newXScale(d.x))
|
| 478 |
+
.y0(d => newYScale(d.yLower))
|
| 479 |
+
.y1(d => newYScale(d.yUpper))
|
| 480 |
+
.curve(d3.curveMonotoneX);
|
| 481 |
+
|
| 482 |
+
gPlot.selectAll('path.uncertainty-band')
|
| 483 |
+
.attr('d', area(dataPoints));
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
// Update line
|
| 487 |
+
const line = d3.line()
|
| 488 |
+
.x(d => newXScale(d.x))
|
| 489 |
+
.y(d => newYScale(d.y))
|
| 490 |
+
.curve(d3.curveMonotoneX);
|
| 491 |
+
|
| 492 |
+
gPlot.selectAll('path.main-line')
|
| 493 |
+
.attr('d', line(dataPoints));
|
| 494 |
+
|
| 495 |
+
// Update axes
|
| 496 |
+
gAxes.select('.x-axis').call(d3.axisBottom(newXScale).ticks(5).tickSizeOuter(0).tickFormat(formatX));
|
| 497 |
+
gAxes.select('.y-axis').call(d3.axisLeft(newYScale).ticks(5).tickSizeOuter(0).tickFormat(formatY));
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
function updateResetButton() {
|
| 501 |
+
if (hasMoved) {
|
| 502 |
+
resetBtn.style.display = 'block';
|
| 503 |
+
requestAnimationFrame(() => { resetBtn.style.opacity = '1'; });
|
| 504 |
+
} else {
|
| 505 |
+
resetBtn.style.opacity = '0';
|
| 506 |
+
setTimeout(() => { if (!hasMoved) resetBtn.style.display = 'none'; }, 200);
|
| 507 |
+
}
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
function render() {
|
| 511 |
+
const rect = bodyEl.getBoundingClientRect();
|
| 512 |
+
const width = Math.max(1, Math.round(rect.width || 400));
|
| 513 |
+
const height = CONFIG.chartHeight;
|
| 514 |
+
svg.attr('width', width).attr('height', height);
|
| 515 |
+
|
| 516 |
+
const margin = CONFIG.margin;
|
| 517 |
+
const innerWidth = width - margin.left - margin.right;
|
| 518 |
+
const innerHeight = height - margin.top - margin.bottom;
|
| 519 |
+
|
| 520 |
+
g.attr('transform', `translate(${margin.left},${margin.top})`);
|
| 521 |
+
|
| 522 |
+
// Filter and prepare data for this metric
|
| 523 |
+
// Support both mean/std columns and direct value columns
|
| 524 |
+
const meanKey = `${metricKey}_mean`;
|
| 525 |
+
const stdKey = `${metricKey}_std`;
|
| 526 |
+
hasMeanStd = allData.some(d => d[meanKey] != null && d[stdKey] != null);
|
| 527 |
+
|
| 528 |
+
if (hasMeanStd) {
|
| 529 |
+
// Data has mean and std columns
|
| 530 |
+
dataPoints = allData
|
| 531 |
+
.filter(d => d[CONFIG.xColumn] != null && !isNaN(d[CONFIG.xColumn]) &&
|
| 532 |
+
d[meanKey] != null && !isNaN(d[meanKey]) &&
|
| 533 |
+
d[stdKey] != null && !isNaN(d[stdKey]))
|
| 534 |
+
.map(d => ({
|
| 535 |
+
x: +d[CONFIG.xColumn],
|
| 536 |
+
y: +d[meanKey],
|
| 537 |
+
yUpper: +d[meanKey] + +d[stdKey],
|
| 538 |
+
yLower: +d[meanKey] - +d[stdKey]
|
| 539 |
+
}))
|
| 540 |
+
.sort((a, b) => a.x - b.x);
|
| 541 |
+
} else {
|
| 542 |
+
// Data has direct value columns (fallback)
|
| 543 |
+
dataPoints = allData
|
| 544 |
+
.filter(d => d[CONFIG.xColumn] != null && !isNaN(d[CONFIG.xColumn]) && d[metricKey] != null && !isNaN(d[metricKey]))
|
| 545 |
+
.map(d => ({ x: +d[CONFIG.xColumn], y: +d[metricKey] }))
|
| 546 |
+
.sort((a, b) => a.x - b.x);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
if (!dataPoints.length) {
|
| 550 |
+
return;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
// Auto-compute domains from data
|
| 554 |
+
const xExtent = d3.extent(dataPoints, d => d.x);
|
| 555 |
+
const yExtent = hasMeanStd
|
| 556 |
+
? d3.extent([...dataPoints.map(d => d.yUpper), ...dataPoints.map(d => d.yLower)])
|
| 557 |
+
: d3.extent(dataPoints, d => d.y);
|
| 558 |
+
|
| 559 |
+
// Ensure Y axis never goes below 0
|
| 560 |
+
const yDomain = [Math.max(0, yExtent[0]), yExtent[1]];
|
| 561 |
+
|
| 562 |
+
xScale.domain(xExtent).range([0, innerWidth]);
|
| 563 |
+
yScale.domain(yDomain).range([innerHeight, 0]);
|
| 564 |
+
|
| 565 |
+
// Create smart formatters based on actual data
|
| 566 |
+
const xValues = dataPoints.map(d => d.x);
|
| 567 |
+
const yValues = dataPoints.map(d => d.y);
|
| 568 |
+
formatX = createSmartFormatter(xValues);
|
| 569 |
+
formatY = createSmartFormatter(yValues);
|
| 570 |
+
|
| 571 |
+
// Update clip
|
| 572 |
+
clipRect.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 573 |
+
|
| 574 |
+
// Update overlay
|
| 575 |
+
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight);
|
| 576 |
+
|
| 577 |
+
// Update zoom extent
|
| 578 |
+
zoom.extent([[0, 0], [innerWidth, innerHeight]])
|
| 579 |
+
.translateExtent([[0, 0], [innerWidth, innerHeight]]);
|
| 580 |
+
|
| 581 |
+
// Grid
|
| 582 |
+
gGrid.selectAll('line').data(yScale.ticks(5)).join('line')
|
| 583 |
+
.attr('x1', 0).attr('x2', innerWidth)
|
| 584 |
+
.attr('y1', d => yScale(d)).attr('y2', d => yScale(d))
|
| 585 |
+
.attr('stroke', 'var(--grid-color)');
|
| 586 |
+
|
| 587 |
+
// Axes
|
| 588 |
+
gAxes.selectAll('*').remove();
|
| 589 |
+
gAxes.append('g').attr('class', 'x-axis').attr('transform', `translate(0,${innerHeight})`)
|
| 590 |
+
.call(d3.axisBottom(xScale).ticks(5).tickSizeOuter(0).tickFormat(formatX));
|
| 591 |
+
gAxes.append('g').attr('class', 'y-axis')
|
| 592 |
+
.call(d3.axisLeft(yScale).ticks(5).tickSizeOuter(0).tickFormat(formatY));
|
| 593 |
+
gAxes.selectAll('.domain, .tick line').attr('stroke', 'var(--axis-color)');
|
| 594 |
+
gAxes.selectAll('text').attr('fill', 'var(--tick-color)');
|
| 595 |
+
|
| 596 |
+
// Axis labels
|
| 597 |
+
gAxes.append('text')
|
| 598 |
+
.attr('class', 'axis-label')
|
| 599 |
+
.attr('x', innerWidth / 2)
|
| 600 |
+
.attr('y', innerHeight + 32)
|
| 601 |
+
.attr('text-anchor', 'middle')
|
| 602 |
+
.text(CONFIG.xAxisLabel);
|
| 603 |
+
|
| 604 |
+
gAxes.append('text')
|
| 605 |
+
.attr('class', 'axis-label')
|
| 606 |
+
.attr('transform', 'rotate(-90)')
|
| 607 |
+
.attr('x', -innerHeight / 2)
|
| 608 |
+
.attr('y', -38)
|
| 609 |
+
.attr('text-anchor', 'middle')
|
| 610 |
+
.text(metricConfig.yAxisLabel || 'Value');
|
| 611 |
+
|
| 612 |
+
// Uncertainty band (if mean/std available)
|
| 613 |
+
if (hasMeanStd) {
|
| 614 |
+
const area = d3.area()
|
| 615 |
+
.x(d => xScale(d.x))
|
| 616 |
+
.y0(d => yScale(d.yLower))
|
| 617 |
+
.y1(d => yScale(d.yUpper))
|
| 618 |
+
.curve(d3.curveMonotoneX);
|
| 619 |
+
|
| 620 |
+
gPlot.selectAll('path.uncertainty-band').data([dataPoints]).join('path')
|
| 621 |
+
.attr('class', 'uncertainty-band')
|
| 622 |
+
.attr('d', area);
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
// Main line
|
| 626 |
+
const mainLine = d3.line()
|
| 627 |
+
.x(d => xScale(d.x))
|
| 628 |
+
.y(d => yScale(d.y))
|
| 629 |
+
.curve(d3.curveMonotoneX);
|
| 630 |
+
|
| 631 |
+
gPlot.selectAll('path.main-line').data([dataPoints]).join('path')
|
| 632 |
+
.attr('class', 'main-line')
|
| 633 |
+
.attr('fill', 'none')
|
| 634 |
+
.attr('stroke', CONFIG.lineColor)
|
| 635 |
+
.attr('stroke-width', 2)
|
| 636 |
+
.attr('opacity', 0.85)
|
| 637 |
+
.attr('d', mainLine);
|
| 638 |
+
|
| 639 |
+
// Hover
|
| 640 |
+
setupHover(innerWidth, innerHeight);
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
function setupHover(innerWidth, innerHeight) {
|
| 644 |
+
gHover.selectAll('*').remove();
|
| 645 |
+
|
| 646 |
+
hoverLine = gHover.append('line')
|
| 647 |
+
.style('stroke', 'var(--text-color)')
|
| 648 |
+
.attr('stroke-opacity', 0.25)
|
| 649 |
+
.attr('stroke-width', 1)
|
| 650 |
+
.attr('y1', 0)
|
| 651 |
+
.attr('y2', innerHeight)
|
| 652 |
+
.style('display', 'none')
|
| 653 |
+
.attr('pointer-events', 'none');
|
| 654 |
+
|
| 655 |
+
overlay.on('mousemove', function (ev) {
|
| 656 |
+
if (ev.buttons === 0) onHoverMove(ev);
|
| 657 |
+
}).on('mouseleave', onHoverLeave);
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
function onHoverMove(ev) {
|
| 661 |
+
if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; }
|
| 662 |
+
|
| 663 |
+
const [mx, my] = d3.pointer(ev, overlay.node());
|
| 664 |
+
const targetX = xScale.invert(mx);
|
| 665 |
+
|
| 666 |
+
// Find nearest data point
|
| 667 |
+
let nearest = dataPoints[0];
|
| 668 |
+
let minDist = Math.abs(dataPoints[0].x - targetX);
|
| 669 |
+
for (let i = 1; i < dataPoints.length; i++) {
|
| 670 |
+
const dist = Math.abs(dataPoints[i].x - targetX);
|
| 671 |
+
if (dist < minDist) {
|
| 672 |
+
minDist = dist;
|
| 673 |
+
nearest = dataPoints[i];
|
| 674 |
+
}
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
const xpx = xScale(nearest.x);
|
| 678 |
+
hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null);
|
| 679 |
+
|
| 680 |
+
let html = `<div><strong>${metricConfig.label}</strong></div>`;
|
| 681 |
+
html += `<div>α = ${formatX(nearest.x)}</div>`;
|
| 682 |
+
if (hasMeanStd && nearest.yUpper != null && nearest.yLower != null) {
|
| 683 |
+
html += `<div>Mean: ${formatY(nearest.y)}</div>`;
|
| 684 |
+
html += `<div>± 1 std dev: [${formatY(nearest.yLower)}, ${formatY(nearest.yUpper)}]</div>`;
|
| 685 |
+
} else {
|
| 686 |
+
html += `<div>${metricConfig.yAxisLabel || 'Value'}: ${formatY(nearest.y)}</div>`;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
tipInner.innerHTML = html;
|
| 690 |
+
const offsetX = 12, offsetY = 12;
|
| 691 |
+
tip.style.opacity = '1';
|
| 692 |
+
tip.style.transform = `translate(${Math.round(mx + offsetX + CONFIG.margin.left)}px, ${Math.round(my + offsetY + CONFIG.margin.top)}px)`;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
function onHoverLeave() {
|
| 696 |
+
hideTipTimer = setTimeout(() => {
|
| 697 |
+
tip.style.opacity = '0';
|
| 698 |
+
tip.style.transform = 'translate(-9999px, -9999px)';
|
| 699 |
+
if (hoverLine) hoverLine.style('display', 'none');
|
| 700 |
+
}, 100);
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
// Reset button
|
| 704 |
+
resetBtn.addEventListener('click', () => {
|
| 705 |
+
overlay.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
|
| 706 |
+
});
|
| 707 |
+
|
| 708 |
+
return { render };
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
// Transform long format CSV to wide format
|
| 712 |
+
function transformLongToWide(longData) {
|
| 713 |
+
// Mapping from CSV quantity names to embed metric keys
|
| 714 |
+
const quantityMap = {
|
| 715 |
+
'llm_score_concept': 'concept_inclusion',
|
| 716 |
+
'eiffel': 'eiffel',
|
| 717 |
+
'llm_score_instruction': 'instruction_following',
|
| 718 |
+
'surprise': 'surprise',
|
| 719 |
+
'llm_score_fluency': 'fluency',
|
| 720 |
+
'rep3': 'repetition'
|
| 721 |
+
};
|
| 722 |
+
|
| 723 |
+
// Group by steering_intensity
|
| 724 |
+
const grouped = {};
|
| 725 |
+
longData.forEach(row => {
|
| 726 |
+
const intensity = parseFloat(row.steering_intensity);
|
| 727 |
+
if (isNaN(intensity)) return;
|
| 728 |
+
|
| 729 |
+
if (!grouped[intensity]) {
|
| 730 |
+
grouped[intensity] = { alpha: intensity, steering_intensity: intensity };
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
const quantity = row.quantity;
|
| 734 |
+
const statType = row.stat_type;
|
| 735 |
+
const value = parseFloat(row.value);
|
| 736 |
+
|
| 737 |
+
if (isNaN(value)) return;
|
| 738 |
+
|
| 739 |
+
// Map quantity name to metric key
|
| 740 |
+
const metricKey = quantityMap[quantity] || quantity;
|
| 741 |
+
|
| 742 |
+
// Store mean and std
|
| 743 |
+
if (statType === 'mean') {
|
| 744 |
+
grouped[intensity][`${metricKey}_mean`] = value;
|
| 745 |
+
} else if (statType === 'std') {
|
| 746 |
+
grouped[intensity][`${metricKey}_std`] = value;
|
| 747 |
+
}
|
| 748 |
+
});
|
| 749 |
+
|
| 750 |
+
return Object.values(grouped);
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
// Load data
|
| 754 |
+
async function load() {
|
| 755 |
+
try {
|
| 756 |
+
const fetchFirstAvailable = async (paths) => {
|
| 757 |
+
for (const p of paths) {
|
| 758 |
+
try {
|
| 759 |
+
const r = await fetch(p, { cache: 'no-cache' });
|
| 760 |
+
if (r.ok) return await r.text();
|
| 761 |
+
} catch(_){}
|
| 762 |
+
}
|
| 763 |
+
throw new Error('CSV not found at any of the paths: ' + paths.join(', '));
|
| 764 |
+
};
|
| 765 |
+
|
| 766 |
+
const csvText = await fetchFirstAvailable(CONFIG.csvPaths);
|
| 767 |
+
const rawData = d3.csvParse(csvText);
|
| 768 |
+
|
| 769 |
+
// Check if data is in long format (has quantity, stat_type, value columns)
|
| 770 |
+
const isLongFormat = rawData.length > 0 &&
|
| 771 |
+
rawData[0].hasOwnProperty('quantity') &&
|
| 772 |
+
rawData[0].hasOwnProperty('stat_type') &&
|
| 773 |
+
rawData[0].hasOwnProperty('value');
|
| 774 |
+
|
| 775 |
+
if (isLongFormat) {
|
| 776 |
+
allData = transformLongToWide(rawData);
|
| 777 |
+
// Update xColumn to use steering_intensity if available
|
| 778 |
+
if (allData.length > 0 && allData[0].steering_intensity != null) {
|
| 779 |
+
CONFIG.xColumn = 'steering_intensity';
|
| 780 |
+
}
|
| 781 |
+
} else {
|
| 782 |
+
allData = rawData;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
// Init all charts
|
| 786 |
+
const cells = Array.from(grid.querySelectorAll('.chart-cell'));
|
| 787 |
+
const chartInstances = cells.map((cell, idx) => initChart(cell, CONFIG.metrics[idx]));
|
| 788 |
+
|
| 789 |
+
// Render all
|
| 790 |
+
chartInstances.forEach(chart => chart.render());
|
| 791 |
+
|
| 792 |
+
// Update legend (once for the whole group)
|
| 793 |
+
const hasMeanStd = allData.some(d => {
|
| 794 |
+
return CONFIG.metrics.some(m => {
|
| 795 |
+
const meanKey = `${m.key}_mean`;
|
| 796 |
+
const stdKey = `${m.key}_std`;
|
| 797 |
+
return d[meanKey] != null && d[stdKey] != null;
|
| 798 |
+
});
|
| 799 |
+
});
|
| 800 |
+
|
| 801 |
+
if (hasMeanStd) {
|
| 802 |
+
legend.innerHTML = `
|
| 803 |
+
<div class="d3-sweep-1d__legend-item">
|
| 804 |
+
<div class="d3-sweep-1d__legend-line" style="background: ${CONFIG.lineColor};"></div>
|
| 805 |
+
<span>Mean</span>
|
| 806 |
+
</div>
|
| 807 |
+
<div class="d3-sweep-1d__legend-item">
|
| 808 |
+
<div class="d3-sweep-1d__legend-band" style="background: ${CONFIG.lineColor}; opacity: 0.2;"></div>
|
| 809 |
+
<span>± 1 std dev</span>
|
| 810 |
+
</div>
|
| 811 |
+
`;
|
| 812 |
+
} else {
|
| 813 |
+
legend.innerHTML = `
|
| 814 |
+
<div class="d3-sweep-1d__legend-item">
|
| 815 |
+
<div class="d3-sweep-1d__legend-line" style="background: ${CONFIG.lineColor};"></div>
|
| 816 |
+
<span>Mean</span>
|
| 817 |
+
</div>
|
| 818 |
+
`;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
// Responsive - observe container for resize
|
| 822 |
+
let resizeTimer;
|
| 823 |
+
const handleResize = () => {
|
| 824 |
+
clearTimeout(resizeTimer);
|
| 825 |
+
resizeTimer = setTimeout(() => {
|
| 826 |
+
chartInstances.forEach(chart => chart.render());
|
| 827 |
+
}, 100);
|
| 828 |
+
};
|
| 829 |
+
|
| 830 |
+
const ro = window.ResizeObserver ? new ResizeObserver(handleResize) : null;
|
| 831 |
+
if (ro) {
|
| 832 |
+
ro.observe(container);
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
// Also observe window resize as fallback
|
| 836 |
+
window.addEventListener('resize', handleResize);
|
| 837 |
+
|
| 838 |
+
// Force a re-render after a short delay to ensure proper sizing
|
| 839 |
+
setTimeout(() => {
|
| 840 |
+
chartInstances.forEach(chart => chart.render());
|
| 841 |
+
}, 100);
|
| 842 |
+
|
| 843 |
+
} catch (e) {
|
| 844 |
+
const pre = document.createElement('pre');
|
| 845 |
+
pre.textContent = 'Error loading data: ' + (e && e.message ? e.message : e);
|
| 846 |
+
pre.style.color = 'var(--danger, #b00020)';
|
| 847 |
+
pre.style.fontSize = '12px';
|
| 848 |
+
container.appendChild(pre);
|
| 849 |
+
}
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
load();
|
| 853 |
+
};
|
| 854 |
+
|
| 855 |
+
if (document.readyState === 'loading') {
|
| 856 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 857 |
+
} else {
|
| 858 |
+
ensureD3(bootstrap);
|
| 859 |
+
}
|
| 860 |
+
})();
|
| 861 |
+
</script>
|
| 862 |
+
|
app/src/styles/_variables.css
CHANGED
|
@@ -11,7 +11,7 @@
|
|
| 11 |
--default-font-family: Source Sans Pro, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
| 12 |
|
| 13 |
/* Brand (OKLCH base + derived states) */
|
| 14 |
-
--primary-base: oklch(0.75 0.12
|
| 15 |
--primary-color: var(--primary-base);
|
| 16 |
--primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h);
|
| 17 |
--primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
|
|
|
|
| 11 |
--default-font-family: Source Sans Pro, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
| 12 |
|
| 13 |
/* Brand (OKLCH base + derived states) */
|
| 14 |
+
--primary-base: oklch(0.75 0.12 47);
|
| 15 |
--primary-color: var(--primary-base);
|
| 16 |
--primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h);
|
| 17 |
--primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
|