Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
fix file size in line charts, update wrong reasons, add train model decision flowchart
Browse files
app/src/components/Image.astro
CHANGED
|
@@ -42,8 +42,40 @@ const {
|
|
| 42 |
linkTarget,
|
| 43 |
linkRel,
|
| 44 |
fullWidth,
|
|
|
|
| 45 |
...imgProps
|
| 46 |
} = Astro.props as Props;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
const hasCaptionSlot = Astro.slots.has("caption");
|
| 48 |
const hasCaption =
|
| 49 |
hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
|
|
@@ -56,6 +88,9 @@ const dataDownloadable =
|
|
| 56 |
const hasLink = typeof linkHref === "string" && linkHref.length > 0;
|
| 57 |
const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;
|
| 58 |
const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
|
|
|
|
|
|
|
|
|
| 59 |
---
|
| 60 |
|
| 61 |
<div
|
|
@@ -79,20 +114,22 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
|
| 79 |
rel={resolvedRel}
|
| 80 |
>
|
| 81 |
<AstroImage
|
|
|
|
| 82 |
{...imgProps}
|
| 83 |
data-zoomable={dataZoomable}
|
| 84 |
data-downloadable={dataDownloadable}
|
| 85 |
data-download-name={downloadName}
|
| 86 |
-
data-download-src={
|
| 87 |
/>
|
| 88 |
</a>
|
| 89 |
) : (
|
| 90 |
<AstroImage
|
|
|
|
| 91 |
{...imgProps}
|
| 92 |
data-zoomable={dataZoomable}
|
| 93 |
data-downloadable={dataDownloadable}
|
| 94 |
data-download-name={downloadName}
|
| 95 |
-
data-download-src={
|
| 96 |
/>
|
| 97 |
)}
|
| 98 |
<button
|
|
@@ -115,10 +152,10 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
|
| 115 |
target={resolvedTarget}
|
| 116 |
rel={resolvedRel}
|
| 117 |
>
|
| 118 |
-
<AstroImage {...imgProps} data-zoomable={dataZoomable} />
|
| 119 |
</a>
|
| 120 |
) : (
|
| 121 |
-
<AstroImage {...imgProps} data-zoomable={dataZoomable} />
|
| 122 |
)}
|
| 123 |
<figcaption>
|
| 124 |
{hasCaptionSlot ? (
|
|
@@ -138,20 +175,22 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
|
| 138 |
rel={resolvedRel}
|
| 139 |
>
|
| 140 |
<AstroImage
|
|
|
|
| 141 |
{...imgProps}
|
| 142 |
data-zoomable={dataZoomable}
|
| 143 |
data-downloadable={dataDownloadable}
|
| 144 |
data-download-name={downloadName}
|
| 145 |
-
data-download-src={
|
| 146 |
/>
|
| 147 |
</a>
|
| 148 |
) : (
|
| 149 |
<AstroImage
|
|
|
|
| 150 |
{...imgProps}
|
| 151 |
data-zoomable={dataZoomable}
|
| 152 |
data-downloadable={dataDownloadable}
|
| 153 |
data-download-name={downloadName}
|
| 154 |
-
data-download-src={
|
| 155 |
/>
|
| 156 |
)}
|
| 157 |
<button
|
|
@@ -173,6 +212,7 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
|
| 173 |
rel={resolvedRel}
|
| 174 |
>
|
| 175 |
<AstroImage
|
|
|
|
| 176 |
{...imgProps}
|
| 177 |
data-zoomable={dataZoomable}
|
| 178 |
class={fullWidth ? "full" : ""}
|
|
@@ -180,6 +220,7 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
|
| 180 |
</a>
|
| 181 |
) : (
|
| 182 |
<AstroImage
|
|
|
|
| 183 |
{...imgProps}
|
| 184 |
data-zoomable={dataZoomable}
|
| 185 |
class={fullWidth ? "full" : ""}
|
|
|
|
| 42 |
linkTarget,
|
| 43 |
linkRel,
|
| 44 |
fullWidth,
|
| 45 |
+
src,
|
| 46 |
...imgProps
|
| 47 |
} = Astro.props as Props;
|
| 48 |
+
|
| 49 |
+
// Try to get original image src from ImageMetadata object
|
| 50 |
+
const getOriginalSrc = (imageSrc: any): string => {
|
| 51 |
+
if (typeof imageSrc === "string") {
|
| 52 |
+
return imageSrc;
|
| 53 |
+
}
|
| 54 |
+
// ImageMetadata from astro:assets has a 'src' property with the URL
|
| 55 |
+
if (imageSrc && typeof imageSrc === "object") {
|
| 56 |
+
// Check if this is an import with ?url (returns the original path directly)
|
| 57 |
+
if ("default" in imageSrc && typeof imageSrc.default === "string") {
|
| 58 |
+
return imageSrc.default;
|
| 59 |
+
}
|
| 60 |
+
// Try src property (optimized version)
|
| 61 |
+
if ("src" in imageSrc && typeof imageSrc.src === "string") {
|
| 62 |
+
return imageSrc.src;
|
| 63 |
+
}
|
| 64 |
+
// Try pathname property
|
| 65 |
+
if ("pathname" in imageSrc && typeof imageSrc.pathname === "string") {
|
| 66 |
+
return imageSrc.pathname;
|
| 67 |
+
}
|
| 68 |
+
// Fallback: try all string properties
|
| 69 |
+
for (const key of Object.keys(imageSrc)) {
|
| 70 |
+
if (typeof imageSrc[key] === "string" && imageSrc[key].length > 0) {
|
| 71 |
+
return imageSrc[key];
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
return "";
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
const originalSrc = getOriginalSrc(src);
|
| 79 |
const hasCaptionSlot = Astro.slots.has("caption");
|
| 80 |
const hasCaption =
|
| 81 |
hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
|
|
|
|
| 88 |
const hasLink = typeof linkHref === "string" && linkHref.length > 0;
|
| 89 |
const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;
|
| 90 |
const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
|
| 91 |
+
|
| 92 |
+
// Use downloadSrc if provided, otherwise try to use the original src
|
| 93 |
+
const resolvedDownloadSrc = downloadSrc || originalSrc;
|
| 94 |
---
|
| 95 |
|
| 96 |
<div
|
|
|
|
| 114 |
rel={resolvedRel}
|
| 115 |
>
|
| 116 |
<AstroImage
|
| 117 |
+
src={src}
|
| 118 |
{...imgProps}
|
| 119 |
data-zoomable={dataZoomable}
|
| 120 |
data-downloadable={dataDownloadable}
|
| 121 |
data-download-name={downloadName}
|
| 122 |
+
data-download-src={resolvedDownloadSrc}
|
| 123 |
/>
|
| 124 |
</a>
|
| 125 |
) : (
|
| 126 |
<AstroImage
|
| 127 |
+
src={src}
|
| 128 |
{...imgProps}
|
| 129 |
data-zoomable={dataZoomable}
|
| 130 |
data-downloadable={dataDownloadable}
|
| 131 |
data-download-name={downloadName}
|
| 132 |
+
data-download-src={resolvedDownloadSrc}
|
| 133 |
/>
|
| 134 |
)}
|
| 135 |
<button
|
|
|
|
| 152 |
target={resolvedTarget}
|
| 153 |
rel={resolvedRel}
|
| 154 |
>
|
| 155 |
+
<AstroImage src={src} {...imgProps} data-zoomable={dataZoomable} />
|
| 156 |
</a>
|
| 157 |
) : (
|
| 158 |
+
<AstroImage src={src} {...imgProps} data-zoomable={dataZoomable} />
|
| 159 |
)}
|
| 160 |
<figcaption>
|
| 161 |
{hasCaptionSlot ? (
|
|
|
|
| 175 |
rel={resolvedRel}
|
| 176 |
>
|
| 177 |
<AstroImage
|
| 178 |
+
src={src}
|
| 179 |
{...imgProps}
|
| 180 |
data-zoomable={dataZoomable}
|
| 181 |
data-downloadable={dataDownloadable}
|
| 182 |
data-download-name={downloadName}
|
| 183 |
+
data-download-src={resolvedDownloadSrc}
|
| 184 |
/>
|
| 185 |
</a>
|
| 186 |
) : (
|
| 187 |
<AstroImage
|
| 188 |
+
src={src}
|
| 189 |
{...imgProps}
|
| 190 |
data-zoomable={dataZoomable}
|
| 191 |
data-downloadable={dataDownloadable}
|
| 192 |
data-download-name={downloadName}
|
| 193 |
+
data-download-src={resolvedDownloadSrc}
|
| 194 |
/>
|
| 195 |
)}
|
| 196 |
<button
|
|
|
|
| 212 |
rel={resolvedRel}
|
| 213 |
>
|
| 214 |
<AstroImage
|
| 215 |
+
src={src}
|
| 216 |
{...imgProps}
|
| 217 |
data-zoomable={dataZoomable}
|
| 218 |
class={fullWidth ? "full" : ""}
|
|
|
|
| 220 |
</a>
|
| 221 |
) : (
|
| 222 |
<AstroImage
|
| 223 |
+
src={src}
|
| 224 |
{...imgProps}
|
| 225 |
data-zoomable={dataZoomable}
|
| 226 |
class={fullWidth ? "full" : ""}
|
app/src/components/Note.astro
CHANGED
|
@@ -54,7 +54,8 @@ const hasHeader =
|
|
| 54 |
.note {
|
| 55 |
background: var(--surface-bg);
|
| 56 |
border-left: 2px solid var(--border-color);
|
| 57 |
-
border-radius: 4px;
|
|
|
|
| 58 |
padding: 10px 14px;
|
| 59 |
margin: var(--block-spacing-y) 0;
|
| 60 |
}
|
|
|
|
| 54 |
.note {
|
| 55 |
background: var(--surface-bg);
|
| 56 |
border-left: 2px solid var(--border-color);
|
| 57 |
+
border-radius-top-right: 4px;
|
| 58 |
+
border-radius-bottom-right: 4px;
|
| 59 |
padding: 10px 14px;
|
| 60 |
margin: var(--block-spacing-y) 0;
|
| 61 |
}
|
app/src/content/embeds/d3-line-chart.html
CHANGED
|
@@ -204,34 +204,7 @@
|
|
| 204 |
flex-wrap: wrap;
|
| 205 |
}
|
| 206 |
|
| 207 |
-
/* Trackio footer */
|
| 208 |
-
.d3-line-chart .trackio-footer {
|
| 209 |
-
text-align: left;
|
| 210 |
-
margin-top: 12px;
|
| 211 |
-
margin-bottom: -2px;
|
| 212 |
-
padding: 0;
|
| 213 |
-
opacity: 0.35;
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
@media (prefers-color-scheme: light) {
|
| 217 |
-
.d3-line-chart .trackio-footer {
|
| 218 |
-
opacity: 0.7;
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
.d3-line-chart .trackio-footer a {
|
| 223 |
-
font-size: 9px;
|
| 224 |
-
color: var(--muted-color);
|
| 225 |
-
opacity: 1;
|
| 226 |
-
text-decoration: none;
|
| 227 |
-
font-weight: 400;
|
| 228 |
-
letter-spacing: 0.3px;
|
| 229 |
-
transition: opacity 0.2s ease;
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
.d3-line-chart .trackio-footer a:hover {
|
| 233 |
-
opacity: 0.6;
|
| 234 |
-
}
|
| 235 |
</style>
|
| 236 |
<script>
|
| 237 |
(() => {
|
|
@@ -320,11 +293,7 @@
|
|
| 320 |
container.appendChild(chartCard);
|
| 321 |
container.appendChild(header);
|
| 322 |
|
| 323 |
-
//
|
| 324 |
-
const footer = document.createElement('div');
|
| 325 |
-
footer.className = 'trackio-footer';
|
| 326 |
-
footer.innerHTML = '<a href="https://github.com/gradio-app/trackio" target="_blank" rel="noopener noreferrer">Made with Trackio</a>';
|
| 327 |
-
container.appendChild(footer);
|
| 328 |
|
| 329 |
const d3 = window.d3;
|
| 330 |
|
|
@@ -471,6 +440,19 @@
|
|
| 471 |
// Smart formatters (will be set after data loading)
|
| 472 |
let formatX = (v) => v;
|
| 473 |
let formatY = (v) => v;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
// Function to determine smart format based on data values
|
| 476 |
function createSmartFormatter(values, isFileSize = false) {
|
|
@@ -483,18 +465,21 @@
|
|
| 483 |
// Check if all values are effectively integers (within 0.001 tolerance)
|
| 484 |
const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001);
|
| 485 |
|
| 486 |
-
// File size formatting (B,
|
| 487 |
if (isFileSize) {
|
|
|
|
|
|
|
|
|
|
| 488 |
return (v) => {
|
| 489 |
-
if (v >=
|
| 490 |
-
const
|
| 491 |
-
return d3.format('.2f')(
|
| 492 |
-
} else if (v >=
|
| 493 |
-
const
|
| 494 |
-
return d3.format('.2f')(
|
| 495 |
-
} else if (v >=
|
| 496 |
-
const
|
| 497 |
-
return d3.format('.1f')(
|
| 498 |
} else {
|
| 499 |
return d3.format('d')(Math.round(v)) + ' B';
|
| 500 |
}
|
|
@@ -574,9 +559,10 @@
|
|
| 574 |
const innerWidth = xScale.range()[1];
|
| 575 |
|
| 576 |
// Smart grid ticks (same as axis ticks)
|
|
|
|
| 577 |
const getGridTicks = (scale, scaleType) => {
|
| 578 |
if (scaleType !== 'log') {
|
| 579 |
-
return scale.ticks(6);
|
| 580 |
}
|
| 581 |
|
| 582 |
const domain = scale.domain();
|
|
@@ -605,7 +591,8 @@
|
|
| 605 |
ticks.sort((a, b) => a - b);
|
| 606 |
}
|
| 607 |
|
| 608 |
-
|
|
|
|
| 609 |
};
|
| 610 |
|
| 611 |
// Update grid lines
|
|
@@ -640,18 +627,27 @@
|
|
| 640 |
.attr('d', d => line(applySmoothing(d.values, smoothEnabled)));
|
| 641 |
|
| 642 |
// Smart tick generation for zoomed view
|
| 643 |
-
const getSmartTicksZoom = (scale, scaleType) => {
|
| 644 |
if (scaleType !== 'log') {
|
| 645 |
-
return scale.ticks(6);
|
| 646 |
}
|
| 647 |
|
| 648 |
const domain = scale.domain();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
const minLog = Math.log10(domain[0]);
|
| 650 |
const maxLog = Math.log10(domain[1]);
|
| 651 |
const logRange = maxLog - minLog;
|
| 652 |
|
| 653 |
if (logRange < 2) {
|
| 654 |
-
return scale.ticks(Math.min(4, Math.ceil(logRange * 2)));
|
| 655 |
}
|
| 656 |
|
| 657 |
const ticks = [];
|
|
@@ -671,10 +667,11 @@
|
|
| 671 |
ticks.sort((a, b) => a - b);
|
| 672 |
}
|
| 673 |
|
| 674 |
-
|
|
|
|
| 675 |
};
|
| 676 |
|
| 677 |
-
const newXTicks = getSmartTicksZoom(newXScale, CONFIG.xScaleType);
|
| 678 |
const newYTicks = getSmartTicksZoom(newYScale, CONFIG.yScaleType);
|
| 679 |
|
| 680 |
// Update axes with rescaled domains
|
|
@@ -742,33 +739,38 @@
|
|
| 742 |
.translateExtent([[0, 0], [innerWidth, innerHeight]]);
|
| 743 |
|
| 744 |
// Smart tick generation for log scales
|
| 745 |
-
const getSmartTicks = (scale, scaleType, targetCount = 6) => {
|
| 746 |
if (scaleType !== 'log') {
|
| 747 |
-
return scale.ticks(targetCount);
|
| 748 |
}
|
| 749 |
|
| 750 |
-
// For log scale, use custom tick logic to avoid crowding
|
| 751 |
const domain = scale.domain();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 752 |
const minLog = Math.log10(domain[0]);
|
| 753 |
const maxLog = Math.log10(domain[1]);
|
| 754 |
const logRange = maxLog - minLog;
|
| 755 |
|
| 756 |
-
// If range is small (< 2 orders of magnitude), use fewer ticks
|
| 757 |
if (logRange < 2) {
|
| 758 |
-
return scale.ticks(Math.min(4, Math.ceil(logRange * 2)));
|
| 759 |
}
|
| 760 |
|
| 761 |
-
// For larger ranges, show powers of 10 and maybe intermediate values
|
| 762 |
const ticks = [];
|
| 763 |
const minPower = Math.ceil(minLog);
|
| 764 |
const maxPower = Math.floor(maxLog);
|
| 765 |
|
| 766 |
-
// Always include powers of 10
|
| 767 |
for (let power = minPower; power <= maxPower; power++) {
|
| 768 |
ticks.push(Math.pow(10, power));
|
| 769 |
}
|
| 770 |
|
| 771 |
-
// If we have too few ticks and the range is not too large, add intermediate values
|
| 772 |
if (ticks.length < 4 && logRange < 3) {
|
| 773 |
const intermediateTicks = [];
|
| 774 |
for (let power = minPower; power < maxPower; power++) {
|
|
@@ -778,11 +780,11 @@
|
|
| 778 |
ticks.sort((a, b) => a - b);
|
| 779 |
}
|
| 780 |
|
| 781 |
-
|
| 782 |
-
return
|
| 783 |
};
|
| 784 |
|
| 785 |
-
const xTicks = getSmartTicks(xScale, CONFIG.xScaleType, 6);
|
| 786 |
const yTicks = getSmartTicks(yScale, CONFIG.yScaleType, 6);
|
| 787 |
|
| 788 |
// Grid (use smart ticks)
|
|
@@ -1116,6 +1118,9 @@
|
|
| 1116 |
CONFIG.xColumn.toLowerCase().includes('size') ||
|
| 1117 |
CONFIG.xColumn.toLowerCase().includes('message');
|
| 1118 |
|
|
|
|
|
|
|
|
|
|
| 1119 |
formatX = createSmartFormatter(xValues, shouldFormatAsFileSize);
|
| 1120 |
formatY = createSmartFormatter(yValues);
|
| 1121 |
|
|
|
|
| 204 |
flex-wrap: wrap;
|
| 205 |
}
|
| 206 |
|
| 207 |
+
/* Trackio footer removed */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
</style>
|
| 209 |
<script>
|
| 210 |
(() => {
|
|
|
|
| 293 |
container.appendChild(chartCard);
|
| 294 |
container.appendChild(header);
|
| 295 |
|
| 296 |
+
// Footer removed
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
const d3 = window.d3;
|
| 299 |
|
|
|
|
| 440 |
// Smart formatters (will be set after data loading)
|
| 441 |
let formatX = (v) => v;
|
| 442 |
let formatY = (v) => v;
|
| 443 |
+
const MAX_TICKS = 10;
|
| 444 |
+
|
| 445 |
+
function limitTicks(ticks, maxCount, domain) {
|
| 446 |
+
if (!Array.isArray(ticks)) return ticks;
|
| 447 |
+
if (ticks.length <= maxCount) return ticks;
|
| 448 |
+
const first = ticks[0];
|
| 449 |
+
const last = ticks[ticks.length - 1];
|
| 450 |
+
const step = Math.ceil(ticks.length / maxCount);
|
| 451 |
+
const sampled = ticks.filter((_, i) => i % step === 0);
|
| 452 |
+
if (sampled[0] !== first && first >= domain[0]) sampled.unshift(first);
|
| 453 |
+
if (sampled[sampled.length - 1] !== last && last <= domain[1]) sampled.push(last);
|
| 454 |
+
return sampled.slice(0, maxCount);
|
| 455 |
+
}
|
| 456 |
|
| 457 |
// Function to determine smart format based on data values
|
| 458 |
function createSmartFormatter(values, isFileSize = false) {
|
|
|
|
| 465 |
// Check if all values are effectively integers (within 0.001 tolerance)
|
| 466 |
const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001);
|
| 467 |
|
| 468 |
+
// File size formatting (binary units: B, KiB, MiB, GiB)
|
| 469 |
if (isFileSize) {
|
| 470 |
+
const KiB = 1024;
|
| 471 |
+
const MiB = KiB * 1024;
|
| 472 |
+
const GiB = MiB * 1024;
|
| 473 |
return (v) => {
|
| 474 |
+
if (v >= GiB) {
|
| 475 |
+
const gib = v / GiB;
|
| 476 |
+
return (gib % 1 === 0 ? d3.format('d')(gib) : d3.format('.2f')(gib)) + ' GiB';
|
| 477 |
+
} else if (v >= MiB) {
|
| 478 |
+
const mib = v / MiB;
|
| 479 |
+
return (mib % 1 === 0 ? d3.format('d')(mib) : d3.format('.2f')(mib)) + ' MiB';
|
| 480 |
+
} else if (v >= KiB) {
|
| 481 |
+
const kib = v / KiB;
|
| 482 |
+
return (kib % 1 === 0 ? d3.format('d')(kib) : d3.format('.1f')(kib)) + ' KiB';
|
| 483 |
} else {
|
| 484 |
return d3.format('d')(Math.round(v)) + ' B';
|
| 485 |
}
|
|
|
|
| 559 |
const innerWidth = xScale.range()[1];
|
| 560 |
|
| 561 |
// Smart grid ticks (same as axis ticks)
|
| 562 |
+
|
| 563 |
const getGridTicks = (scale, scaleType) => {
|
| 564 |
if (scaleType !== 'log') {
|
| 565 |
+
return scale.ticks(Math.min(6, MAX_TICKS));
|
| 566 |
}
|
| 567 |
|
| 568 |
const domain = scale.domain();
|
|
|
|
| 591 |
ticks.sort((a, b) => a - b);
|
| 592 |
}
|
| 593 |
|
| 594 |
+
const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
|
| 595 |
+
return limitTicks(within, MAX_TICKS, domain);
|
| 596 |
};
|
| 597 |
|
| 598 |
// Update grid lines
|
|
|
|
| 627 |
.attr('d', d => line(applySmoothing(d.values, smoothEnabled)));
|
| 628 |
|
| 629 |
// Smart tick generation for zoomed view
|
| 630 |
+
const getSmartTicksZoom = (scale, scaleType, useBinary = false) => {
|
| 631 |
if (scaleType !== 'log') {
|
| 632 |
+
return scale.ticks(Math.min(6, MAX_TICKS));
|
| 633 |
}
|
| 634 |
|
| 635 |
const domain = scale.domain();
|
| 636 |
+
if (useBinary) {
|
| 637 |
+
const minPow2 = Math.ceil(Math.log2(domain[0]));
|
| 638 |
+
const maxPow2 = Math.floor(Math.log2(domain[1]));
|
| 639 |
+
const ticks = [];
|
| 640 |
+
for (let p = minPow2; p <= maxPow2; p++) ticks.push(Math.pow(2, p));
|
| 641 |
+
const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
|
| 642 |
+
return limitTicks(within, MAX_TICKS, domain);
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
const minLog = Math.log10(domain[0]);
|
| 646 |
const maxLog = Math.log10(domain[1]);
|
| 647 |
const logRange = maxLog - minLog;
|
| 648 |
|
| 649 |
if (logRange < 2) {
|
| 650 |
+
return scale.ticks(Math.min(MAX_TICKS, Math.min(4, Math.ceil(logRange * 2))));
|
| 651 |
}
|
| 652 |
|
| 653 |
const ticks = [];
|
|
|
|
| 667 |
ticks.sort((a, b) => a - b);
|
| 668 |
}
|
| 669 |
|
| 670 |
+
const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
|
| 671 |
+
return limitTicks(within, MAX_TICKS, domain);
|
| 672 |
};
|
| 673 |
|
| 674 |
+
const newXTicks = getSmartTicksZoom(newXScale, CONFIG.xScaleType, !!CONFIG.xFormatAsFileSize);
|
| 675 |
const newYTicks = getSmartTicksZoom(newYScale, CONFIG.yScaleType);
|
| 676 |
|
| 677 |
// Update axes with rescaled domains
|
|
|
|
| 739 |
.translateExtent([[0, 0], [innerWidth, innerHeight]]);
|
| 740 |
|
| 741 |
// Smart tick generation for log scales
|
| 742 |
+
const getSmartTicks = (scale, scaleType, targetCount = 6, useBinary = false) => {
|
| 743 |
if (scaleType !== 'log') {
|
| 744 |
+
return scale.ticks(Math.min(targetCount, MAX_TICKS));
|
| 745 |
}
|
| 746 |
|
|
|
|
| 747 |
const domain = scale.domain();
|
| 748 |
+
if (useBinary) {
|
| 749 |
+
const minPow2 = Math.ceil(Math.log2(domain[0]));
|
| 750 |
+
const maxPow2 = Math.floor(Math.log2(domain[1]));
|
| 751 |
+
const ticks = [];
|
| 752 |
+
for (let p = minPow2; p <= maxPow2; p++) ticks.push(Math.pow(2, p));
|
| 753 |
+
const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
|
| 754 |
+
return limitTicks(within, MAX_TICKS, domain);
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
// For log scale, use custom tick logic to avoid crowding (powers of 10)
|
| 758 |
const minLog = Math.log10(domain[0]);
|
| 759 |
const maxLog = Math.log10(domain[1]);
|
| 760 |
const logRange = maxLog - minLog;
|
| 761 |
|
|
|
|
| 762 |
if (logRange < 2) {
|
| 763 |
+
return scale.ticks(Math.min(MAX_TICKS, Math.min(4, Math.ceil(logRange * 2))));
|
| 764 |
}
|
| 765 |
|
|
|
|
| 766 |
const ticks = [];
|
| 767 |
const minPower = Math.ceil(minLog);
|
| 768 |
const maxPower = Math.floor(maxLog);
|
| 769 |
|
|
|
|
| 770 |
for (let power = minPower; power <= maxPower; power++) {
|
| 771 |
ticks.push(Math.pow(10, power));
|
| 772 |
}
|
| 773 |
|
|
|
|
| 774 |
if (ticks.length < 4 && logRange < 3) {
|
| 775 |
const intermediateTicks = [];
|
| 776 |
for (let power = minPower; power < maxPower; power++) {
|
|
|
|
| 780 |
ticks.sort((a, b) => a - b);
|
| 781 |
}
|
| 782 |
|
| 783 |
+
const within = ticks.filter(t => t >= domain[0] && t <= domain[1]);
|
| 784 |
+
return limitTicks(within, MAX_TICKS, domain);
|
| 785 |
};
|
| 786 |
|
| 787 |
+
const xTicks = getSmartTicks(xScale, CONFIG.xScaleType, 6, !!CONFIG.xFormatAsFileSize);
|
| 788 |
const yTicks = getSmartTicks(yScale, CONFIG.yScaleType, 6);
|
| 789 |
|
| 790 |
// Grid (use smart ticks)
|
|
|
|
| 1118 |
CONFIG.xColumn.toLowerCase().includes('size') ||
|
| 1119 |
CONFIG.xColumn.toLowerCase().includes('message');
|
| 1120 |
|
| 1121 |
+
// Ensure CONFIG flag reflects detection for consistent tick logic
|
| 1122 |
+
CONFIG.xFormatAsFileSize = shouldFormatAsFileSize;
|
| 1123 |
+
|
| 1124 |
formatX = createSmartFormatter(xValues, shouldFormatAsFileSize);
|
| 1125 |
formatY = createSmartFormatter(yValues);
|
| 1126 |
|
app/src/content/embeds/train-model-decision-flowchart.html
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!--
|
| 2 |
+
Train Model Decision Flowchart
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
<HtmlEmbed src="/embeds/train-model-decision-flowchart.html" />
|
| 6 |
+
-->
|
| 7 |
+
<div class="train-model-decision-flowchart"></div>
|
| 8 |
+
<style>
|
| 9 |
+
.train-model-decision-flowchart {
|
| 10 |
+
width: 100%;
|
| 11 |
+
min-height: 300px;
|
| 12 |
+
position: relative;
|
| 13 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.train-model-decision-flowchart svg {
|
| 17 |
+
display: block;
|
| 18 |
+
width: 100%;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.train-model-decision-flowchart .node-rect {
|
| 22 |
+
stroke-width: 2px;
|
| 23 |
+
rx: 12px;
|
| 24 |
+
ry: 12px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.train-model-decision-flowchart .node-text {
|
| 28 |
+
font-size: 18px;
|
| 29 |
+
font-weight: 500;
|
| 30 |
+
text-anchor: middle;
|
| 31 |
+
pointer-events: none;
|
| 32 |
+
fill: var(--text-color, #333);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.train-model-decision-flowchart .node-question {
|
| 36 |
+
fill: color-mix(in srgb, var(--primary-color) 30%, var(--page-bg));
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.train-model-decision-flowchart .node-success {
|
| 40 |
+
fill: color-mix(in srgb, var(--success-color) 30%, var(--page-bg));
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.train-model-decision-flowchart .node-category {
|
| 44 |
+
fill: color-mix(in srgb, var(--danger-color) 25%, var(--page-bg));
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.train-model-decision-flowchart .link-path {
|
| 48 |
+
fill: none;
|
| 49 |
+
stroke: var(--muted-color, #666);
|
| 50 |
+
stroke-width: 2.5px;
|
| 51 |
+
marker-end: url(#arrowhead);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.train-model-decision-flowchart .link-label {
|
| 55 |
+
font-size: 14px;
|
| 56 |
+
font-weight: 700;
|
| 57 |
+
fill: var(--text-color, #333);
|
| 58 |
+
text-anchor: middle;
|
| 59 |
+
pointer-events: none;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.train-model-decision-flowchart .link-label-bg {
|
| 63 |
+
fill: var(--page-bg, #ffffff);
|
| 64 |
+
stroke: none;
|
| 65 |
+
}
|
| 66 |
+
</style>
|
| 67 |
+
<script>
|
| 68 |
+
(() => {
|
| 69 |
+
const ensureD3 = (cb) => {
|
| 70 |
+
if (window.d3 && typeof window.d3.select === 'function') return cb();
|
| 71 |
+
let s = document.getElementById('d3-cdn-script');
|
| 72 |
+
if (!s) {
|
| 73 |
+
s = document.createElement('script');
|
| 74 |
+
s.id = 'd3-cdn-script';
|
| 75 |
+
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';
|
| 76 |
+
document.head.appendChild(s);
|
| 77 |
+
}
|
| 78 |
+
const onReady = () => {
|
| 79 |
+
if (window.d3 && typeof window.d3.select === 'function') cb();
|
| 80 |
+
};
|
| 81 |
+
s.addEventListener('load', onReady, { once: true });
|
| 82 |
+
if (window.d3) onReady();
|
| 83 |
+
};
|
| 84 |
+
|
| 85 |
+
const bootstrap = () => {
|
| 86 |
+
const scriptEl = document.currentScript;
|
| 87 |
+
let container = scriptEl ? scriptEl.previousElementSibling : null;
|
| 88 |
+
if (!(container && container.classList && container.classList.contains('train-model-decision-flowchart'))) {
|
| 89 |
+
const candidates = Array.from(document.querySelectorAll('.train-model-decision-flowchart'))
|
| 90 |
+
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
|
| 91 |
+
container = candidates[candidates.length - 1] || null;
|
| 92 |
+
}
|
| 93 |
+
if (!container) return;
|
| 94 |
+
if (container.dataset) {
|
| 95 |
+
if (container.dataset.mounted === 'true') return;
|
| 96 |
+
container.dataset.mounted = 'true';
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Define color scheme matching the Mermaid chart
|
| 100 |
+
const getColors = () => {
|
| 101 |
+
// Try to use CSS variables first, fallback to Mermaid colors
|
| 102 |
+
const getCSSVar = (varName, fallback) => {
|
| 103 |
+
if (typeof getComputedStyle !== 'undefined') {
|
| 104 |
+
const value = getComputedStyle(document.documentElement)
|
| 105 |
+
.getPropertyValue(varName);
|
| 106 |
+
if (value && value.trim()) {
|
| 107 |
+
return value.trim();
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
return fallback;
|
| 111 |
+
};
|
| 112 |
+
|
| 113 |
+
return {
|
| 114 |
+
question: getCSSVar('--danger-color', '#e85c42'),
|
| 115 |
+
decision: getCSSVar('--surface-bg', '#f9f9f9'),
|
| 116 |
+
success: getCSSVar('--success-color', '#42d9b3'),
|
| 117 |
+
category: getCSSVar('--primary-color', '#0084ff'),
|
| 118 |
+
link: getCSSVar('--muted-color', '#666')
|
| 119 |
+
};
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
// Define the flowchart structure
|
| 123 |
+
// Layout: top-down with very generous vertical spacing between layers
|
| 124 |
+
const nodes = [
|
| 125 |
+
{ id: 'A', label: 'Should you train your own model?', type: 'question', x: 400, y: 100 },
|
| 126 |
+
{ id: 'B', label: 'Can existing models handle your use case?', type: 'question', x: 400, y: 240 },
|
| 127 |
+
{ id: 'C', label: 'Existing models work well just with prompting', type: 'decision', x: 200, y: 400 },
|
| 128 |
+
{ id: 'D', label: 'Prompting isn\'t enough', type: 'decision', x: 600, y: 400 },
|
| 129 |
+
{ id: 'E', label: '❌\nDon\'t train. Use existing models', type: 'success', x: 200, y: 560 },
|
| 130 |
+
{ id: 'F', label: 'Can finetuning solve your problem?', type: 'question', x: 600, y: 600 },
|
| 131 |
+
{ id: 'G', label: 'Finetuning works (post-training/continual pretraining)', type: 'decision', x: 450, y: 760 },
|
| 132 |
+
{ id: 'H', label: 'Finetuning cannot solve your problem', type: 'decision', x: 750, y: 760 },
|
| 133 |
+
{ id: 'I', label: '❌\nDon\'t train from scratch', type: 'success', x: 450, y: 920 },
|
| 134 |
+
{ id: 'J', label: 'Train a model under one of these categories', type: 'category', x: 750, y: 960 },
|
| 135 |
+
{ id: 'K', label: '🔬\nResearch', type: 'category', x: 600, y: 1120 },
|
| 136 |
+
{ id: 'L', label: '🏭\nProduction', type: 'category', x: 750, y: 1120 },
|
| 137 |
+
{ id: 'N', label: '🌐\nStrategic Open-Source', type: 'category', x: 970, y: 1120 }
|
| 138 |
+
];
|
| 139 |
+
|
| 140 |
+
const links = [
|
| 141 |
+
{ source: 'A', target: 'B', label: '' },
|
| 142 |
+
{ source: 'B', target: 'C', label: 'YES' },
|
| 143 |
+
{ source: 'B', target: 'D', label: 'NO' },
|
| 144 |
+
{ source: 'C', target: 'E', label: '' },
|
| 145 |
+
{ source: 'D', target: 'F', label: '' },
|
| 146 |
+
{ source: 'F', target: 'G', label: 'YES' },
|
| 147 |
+
{ source: 'F', target: 'H', label: 'NO' },
|
| 148 |
+
{ source: 'G', target: 'I', label: '' },
|
| 149 |
+
{ source: 'H', target: 'J', label: '' },
|
| 150 |
+
{ source: 'J', target: 'K', label: '' },
|
| 151 |
+
{ source: 'J', target: 'L', label: '' },
|
| 152 |
+
{ source: 'J', target: 'N', label: '' }
|
| 153 |
+
];
|
| 154 |
+
|
| 155 |
+
// Create SVG
|
| 156 |
+
const svg = d3.select(container).append('svg').attr('width', '100%').style('display', 'block');
|
| 157 |
+
const gRoot = svg.append('g');
|
| 158 |
+
|
| 159 |
+
// Define arrowhead marker (solid triangle arrowhead)
|
| 160 |
+
const defs = svg.append('defs');
|
| 161 |
+
const marker = defs.append('marker')
|
| 162 |
+
.attr('id', 'arrowhead')
|
| 163 |
+
.attr('viewBox', '0 0 10 10')
|
| 164 |
+
.attr('refX', 2.5)
|
| 165 |
+
.attr('refY', 5)
|
| 166 |
+
.attr('markerWidth', 4)
|
| 167 |
+
.attr('markerHeight', 4)
|
| 168 |
+
.attr('orient', 'auto');
|
| 169 |
+
|
| 170 |
+
// Create solid arrowhead pointing right (smaller)
|
| 171 |
+
marker.append('path')
|
| 172 |
+
.attr('d', 'M 0 0 L 8 5 L 0 10 Z')
|
| 173 |
+
.attr('fill', () => getColors().link);
|
| 174 |
+
|
| 175 |
+
let width = 1000, height = 800;
|
| 176 |
+
|
| 177 |
+
function render() {
|
| 178 |
+
width = container.clientWidth || 1000;
|
| 179 |
+
height = Math.max(800, Math.round(width * 1.3));
|
| 180 |
+
svg.attr('width', width).attr('height', height);
|
| 181 |
+
|
| 182 |
+
const colors = getColors();
|
| 183 |
+
|
| 184 |
+
// Calculate scale to fit content (no padding, allow to touch edges)
|
| 185 |
+
const nodeExtent = {
|
| 186 |
+
minX: d3.min(nodes, d => d.x) - 140,
|
| 187 |
+
maxX: d3.max(nodes, d => d.x) + 140,
|
| 188 |
+
minY: d3.min(nodes, d => d.y) - 20,
|
| 189 |
+
maxY: d3.max(nodes, d => d.y) + 20
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
const contentWidth = nodeExtent.maxX - nodeExtent.minX;
|
| 193 |
+
const contentHeight = nodeExtent.maxY - nodeExtent.minY;
|
| 194 |
+
|
| 195 |
+
const scale = Math.min(width / contentWidth, height / contentHeight);
|
| 196 |
+
const offsetX = (width - contentWidth * scale) / 2 - nodeExtent.minX * scale;
|
| 197 |
+
const offsetY = (height - contentHeight * scale) / 2 - nodeExtent.minY * scale;
|
| 198 |
+
|
| 199 |
+
gRoot.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
|
| 200 |
+
|
| 201 |
+
// Create a temporary text element for measuring text width
|
| 202 |
+
const tempText = gRoot.append('text')
|
| 203 |
+
.style('visibility', 'hidden')
|
| 204 |
+
.style('font-size', '18px')
|
| 205 |
+
.style('font-weight', '500');
|
| 206 |
+
|
| 207 |
+
// Word wrap function - intelligently breaks text into lines
|
| 208 |
+
const wordWrap = (text, maxWidth, fontSize = '18px') => {
|
| 209 |
+
const explicitLines = text.split('\n');
|
| 210 |
+
const wrappedLines = [];
|
| 211 |
+
|
| 212 |
+
explicitLines.forEach(line => {
|
| 213 |
+
if (!line.trim()) {
|
| 214 |
+
wrappedLines.push(line);
|
| 215 |
+
return;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
tempText.attr('font-size', fontSize).text(line);
|
| 219 |
+
const textWidth = tempText.node().getComputedTextLength();
|
| 220 |
+
|
| 221 |
+
// If line fits, keep it as is
|
| 222 |
+
if (textWidth <= maxWidth) {
|
| 223 |
+
wrappedLines.push(line);
|
| 224 |
+
return;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Otherwise, break into words and wrap
|
| 228 |
+
const words = line.split(/\s+/);
|
| 229 |
+
let currentLine = '';
|
| 230 |
+
|
| 231 |
+
words.forEach(word => {
|
| 232 |
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
| 233 |
+
tempText.text(testLine);
|
| 234 |
+
const testWidth = tempText.node().getComputedTextLength();
|
| 235 |
+
|
| 236 |
+
if (testWidth <= maxWidth && currentLine) {
|
| 237 |
+
currentLine = testLine;
|
| 238 |
+
} else {
|
| 239 |
+
if (currentLine) {
|
| 240 |
+
wrappedLines.push(currentLine);
|
| 241 |
+
}
|
| 242 |
+
currentLine = word;
|
| 243 |
+
}
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
if (currentLine) {
|
| 247 |
+
wrappedLines.push(currentLine);
|
| 248 |
+
}
|
| 249 |
+
});
|
| 250 |
+
|
| 251 |
+
return wrappedLines.filter(line => line.trim().length > 0);
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
// Calculate node dimensions with word wrapping
|
| 255 |
+
const getNodeDimensions = (node) => {
|
| 256 |
+
// Determine max width based on node type (increased for better readability)
|
| 257 |
+
const maxWidths = {
|
| 258 |
+
question: 220,
|
| 259 |
+
decision: 240,
|
| 260 |
+
success: 220,
|
| 261 |
+
category: 200
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
const maxWidth = maxWidths[node.type] || 180;
|
| 265 |
+
const fontSize = node.type === 'category' ? '19px' : '18px';
|
| 266 |
+
|
| 267 |
+
// Get wrapped lines
|
| 268 |
+
const wrappedLines = wordWrap(node.label, maxWidth, fontSize);
|
| 269 |
+
node.wrappedLines = wrappedLines;
|
| 270 |
+
|
| 271 |
+
// Calculate actual width from wrapped lines
|
| 272 |
+
tempText.attr('font-size', fontSize);
|
| 273 |
+
const lineWidths = wrappedLines.map(line => {
|
| 274 |
+
tempText.text(line);
|
| 275 |
+
return tempText.node().getComputedTextLength();
|
| 276 |
+
});
|
| 277 |
+
const maxLineWidth = Math.max(...lineWidths, 0);
|
| 278 |
+
|
| 279 |
+
// Calculate dimensions with padding
|
| 280 |
+
const padding = 36;
|
| 281 |
+
const lineHeight = node.type === 'category' ? 28 : 26;
|
| 282 |
+
|
| 283 |
+
const width = Math.max(120, maxLineWidth + padding);
|
| 284 |
+
const height = Math.max(30, wrappedLines.length * lineHeight + padding);
|
| 285 |
+
|
| 286 |
+
return { width, height, wrappedLines };
|
| 287 |
+
};
|
| 288 |
+
|
| 289 |
+
// Pre-calculate all node dimensions with wrapping
|
| 290 |
+
nodes.forEach(node => {
|
| 291 |
+
const dims = getNodeDimensions(node);
|
| 292 |
+
node.width = dims.width;
|
| 293 |
+
node.height = dims.height;
|
| 294 |
+
});
|
| 295 |
+
|
| 296 |
+
// Keep tempText for measuring link labels (don't remove it yet)
|
| 297 |
+
|
| 298 |
+
// Draw links first (so labels can be on top)
|
| 299 |
+
const linkGroup = gRoot.selectAll('.link-group').data(links);
|
| 300 |
+
const linkEnter = linkGroup.enter().append('g').attr('class', 'link-group');
|
| 301 |
+
|
| 302 |
+
linkEnter.append('path').attr('class', 'link-path');
|
| 303 |
+
// Add background rect and text for labels (will be positioned later, after path)
|
| 304 |
+
linkEnter.append('rect').attr('class', 'link-label-bg').style('opacity', 0);
|
| 305 |
+
linkEnter.append('text').attr('class', 'link-label').attr('dy', -5);
|
| 306 |
+
|
| 307 |
+
const linkMerge = linkEnter.merge(linkGroup);
|
| 308 |
+
|
| 309 |
+
linkMerge.select('.link-path')
|
| 310 |
+
.attr('d', d => {
|
| 311 |
+
const sourceNode = nodes.find(n => n.id === d.source);
|
| 312 |
+
const targetNode = nodes.find(n => n.id === d.target);
|
| 313 |
+
|
| 314 |
+
// Add gap between arrows and nodes, and ensure arrows point to node edges
|
| 315 |
+
const gap = 12;
|
| 316 |
+
|
| 317 |
+
// Calculate arrow start/end points at node edges
|
| 318 |
+
// For vertical connections
|
| 319 |
+
if (Math.abs(sourceNode.x - targetNode.x) < 50) {
|
| 320 |
+
const sx = sourceNode.x;
|
| 321 |
+
const sy = sourceNode.y + sourceNode.height / 2 + gap;
|
| 322 |
+
const tx = targetNode.x;
|
| 323 |
+
const ty = targetNode.y - targetNode.height / 2 - gap;
|
| 324 |
+
return `M ${sx} ${sy} L ${tx} ${ty}`;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
// For horizontal/curved connections
|
| 328 |
+
// Determine connection points based on relative positions
|
| 329 |
+
let sx, sy, tx, ty;
|
| 330 |
+
|
| 331 |
+
if (Math.abs(sourceNode.y - targetNode.y) < 50) {
|
| 332 |
+
// Horizontal connection
|
| 333 |
+
const sourceIsLeft = sourceNode.x < targetNode.x;
|
| 334 |
+
sx = sourceNode.x + (sourceIsLeft ? sourceNode.width / 2 + gap : -(sourceNode.width / 2 + gap));
|
| 335 |
+
sy = sourceNode.y;
|
| 336 |
+
tx = targetNode.x + (sourceIsLeft ? -(targetNode.width / 2 + gap) : targetNode.width / 2 + gap);
|
| 337 |
+
ty = targetNode.y;
|
| 338 |
+
} else {
|
| 339 |
+
// Vertical or diagonal connection
|
| 340 |
+
sx = sourceNode.x;
|
| 341 |
+
sy = sourceNode.y + (sourceNode.y < targetNode.y ? sourceNode.height / 2 + gap : -(sourceNode.height / 2 + gap));
|
| 342 |
+
tx = targetNode.x;
|
| 343 |
+
ty = targetNode.y + (targetNode.y > sourceNode.y ? -(targetNode.height / 2 + gap) : targetNode.height / 2 + gap);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// Use curve for diagonal/horizontal connections
|
| 347 |
+
const midX = (sx + tx) / 2;
|
| 348 |
+
const midY = (sy + ty) / 2;
|
| 349 |
+
return `M ${sx} ${sy} C ${sx} ${midY}, ${tx} ${midY}, ${tx} ${ty}`;
|
| 350 |
+
})
|
| 351 |
+
.attr('stroke', colors.link);
|
| 352 |
+
|
| 353 |
+
// Draw label backgrounds and text (only for non-empty labels like YES/NO)
|
| 354 |
+
linkMerge.filter(d => d.label && d.label.trim())
|
| 355 |
+
.each(function (d) {
|
| 356 |
+
const sourceNode = nodes.find(n => n.id === d.source);
|
| 357 |
+
const targetNode = nodes.find(n => n.id === d.target);
|
| 358 |
+
const x = (sourceNode.x + targetNode.x) / 2;
|
| 359 |
+
const y = (sourceNode.y + targetNode.y) / 2;
|
| 360 |
+
|
| 361 |
+
const labelEl = d3.select(this);
|
| 362 |
+
const textEl = labelEl.select('.link-label');
|
| 363 |
+
|
| 364 |
+
// Measure text to size background (use correct font size for labels)
|
| 365 |
+
tempText.style('font-size', '14px').style('font-weight', '700').text(d.label);
|
| 366 |
+
const textWidth = tempText.node().getComputedTextLength();
|
| 367 |
+
const textHeight = 20;
|
| 368 |
+
const padding = 10;
|
| 369 |
+
|
| 370 |
+
// Position background
|
| 371 |
+
labelEl.select('.link-label-bg')
|
| 372 |
+
.attr('x', x - textWidth / 2 - padding)
|
| 373 |
+
.attr('y', y - textHeight / 2 - padding / 2)
|
| 374 |
+
.attr('width', textWidth + padding * 2)
|
| 375 |
+
.attr('height', textHeight + padding)
|
| 376 |
+
.style('opacity', 1);
|
| 377 |
+
|
| 378 |
+
// Position text
|
| 379 |
+
textEl
|
| 380 |
+
.attr('x', x)
|
| 381 |
+
.attr('y', y)
|
| 382 |
+
.text(d.label);
|
| 383 |
+
});
|
| 384 |
+
|
| 385 |
+
// Empty labels don't need background
|
| 386 |
+
linkMerge.filter(d => !d.label || !d.label.trim())
|
| 387 |
+
.select('.link-label')
|
| 388 |
+
.attr('x', d => {
|
| 389 |
+
const sourceNode = nodes.find(n => n.id === d.source);
|
| 390 |
+
const targetNode = nodes.find(n => n.id === d.target);
|
| 391 |
+
return (sourceNode.x + targetNode.x) / 2;
|
| 392 |
+
})
|
| 393 |
+
.attr('y', d => {
|
| 394 |
+
const sourceNode = nodes.find(n => n.id === d.source);
|
| 395 |
+
const targetNode = nodes.find(n => n.id === d.target);
|
| 396 |
+
return (sourceNode.y + targetNode.y) / 2;
|
| 397 |
+
})
|
| 398 |
+
.text('');
|
| 399 |
+
|
| 400 |
+
// Now remove temporary text element
|
| 401 |
+
tempText.remove();
|
| 402 |
+
|
| 403 |
+
// Draw nodes
|
| 404 |
+
const nodeGroup = gRoot.selectAll('.node-group').data(nodes);
|
| 405 |
+
const nodeEnter = nodeGroup.enter().append('g').attr('class', 'node-group');
|
| 406 |
+
|
| 407 |
+
nodeEnter.append('rect').attr('class', d => `node-rect node-${d.type}`);
|
| 408 |
+
nodeEnter.append('text').attr('class', 'node-text');
|
| 409 |
+
|
| 410 |
+
const nodeMerge = nodeEnter.merge(nodeGroup);
|
| 411 |
+
|
| 412 |
+
nodeMerge.select('.node-rect')
|
| 413 |
+
.attr('x', d => d.x - d.width / 2)
|
| 414 |
+
.attr('y', d => d.y - d.height / 2)
|
| 415 |
+
.attr('width', d => d.width)
|
| 416 |
+
.attr('height', d => d.height)
|
| 417 |
+
.attr('fill', d => {
|
| 418 |
+
// Use CSS classes for colors to support color-mix
|
| 419 |
+
switch (d.type) {
|
| 420 |
+
case 'question': return 'currentColor';
|
| 421 |
+
case 'decision': return colors.decision;
|
| 422 |
+
case 'success': return 'currentColor';
|
| 423 |
+
case 'category': return 'currentColor';
|
| 424 |
+
default: return colors.decision;
|
| 425 |
+
}
|
| 426 |
+
})
|
| 427 |
+
.each(function (d) {
|
| 428 |
+
// Set stroke based on type, using CSS variables
|
| 429 |
+
const strokeColor = d.type === 'decision'
|
| 430 |
+
? 'var(--border-color, #ddd)'
|
| 431 |
+
: 'var(--border-color, #ccc)';
|
| 432 |
+
d3.select(this).attr('stroke', strokeColor);
|
| 433 |
+
})
|
| 434 |
+
.attr('stroke-width', 2);
|
| 435 |
+
|
| 436 |
+
nodeMerge.select('.node-text')
|
| 437 |
+
.attr('x', d => d.x)
|
| 438 |
+
.each(function (d) {
|
| 439 |
+
// Use wrapped lines if available, otherwise fallback to original
|
| 440 |
+
const lines = d.wrappedLines || d.label.split('\n');
|
| 441 |
+
const textEl = d3.select(this);
|
| 442 |
+
textEl.selectAll('tspan').remove();
|
| 443 |
+
|
| 444 |
+
const fontSize = d.type === 'category' ? '19px' : '18px';
|
| 445 |
+
const lineHeight = d.type === 'category' ? 28 : 26;
|
| 446 |
+
|
| 447 |
+
// Calculate vertical centering precisely
|
| 448 |
+
// Position text element at node center Y
|
| 449 |
+
textEl.attr('y', d.y);
|
| 450 |
+
|
| 451 |
+
// Calculate the offset needed to center the text block
|
| 452 |
+
// The first line should be positioned such that the middle of all lines aligns with d.y
|
| 453 |
+
const numLines = lines.length;
|
| 454 |
+
const totalTextHeight = (numLines - 1) * lineHeight;
|
| 455 |
+
|
| 456 |
+
// For each line, calculate its Y position relative to center
|
| 457 |
+
lines.forEach((line, i) => {
|
| 458 |
+
// Calculate offset from center: line 0 is at -totalTextHeight/2, last line at +totalTextHeight/2
|
| 459 |
+
const offsetFromCenter = (i - (numLines - 1) / 2) * lineHeight;
|
| 460 |
+
|
| 461 |
+
textEl.append('tspan')
|
| 462 |
+
.attr('x', d.x)
|
| 463 |
+
.attr('dy', i === 0 ? `${offsetFromCenter}px` : `${lineHeight}px`)
|
| 464 |
+
.attr('font-size', fontSize)
|
| 465 |
+
.attr('text-anchor', 'middle')
|
| 466 |
+
.attr('dominant-baseline', 'central')
|
| 467 |
+
.text(line);
|
| 468 |
+
});
|
| 469 |
+
});
|
| 470 |
+
|
| 471 |
+
// Update arrowhead color
|
| 472 |
+
marker.select('path').attr('fill', colors.link);
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
// Initial render + resize handling
|
| 476 |
+
render();
|
| 477 |
+
const rerender = () => render();
|
| 478 |
+
if (window.ResizeObserver) {
|
| 479 |
+
const ro = new ResizeObserver(() => rerender());
|
| 480 |
+
ro.observe(container);
|
| 481 |
+
} else {
|
| 482 |
+
window.addEventListener('resize', rerender);
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
// Listen for theme changes
|
| 486 |
+
const observer = new MutationObserver(() => {
|
| 487 |
+
render();
|
| 488 |
+
});
|
| 489 |
+
|
| 490 |
+
if (document.documentElement) {
|
| 491 |
+
observer.observe(document.documentElement, {
|
| 492 |
+
attributes: true,
|
| 493 |
+
attributeFilter: ['data-theme', 'class']
|
| 494 |
+
});
|
| 495 |
+
}
|
| 496 |
+
};
|
| 497 |
+
|
| 498 |
+
if (document.readyState === 'loading') {
|
| 499 |
+
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
|
| 500 |
+
} else {
|
| 501 |
+
ensureD3(bootstrap);
|
| 502 |
+
}
|
| 503 |
+
})();
|
| 504 |
+
</script>
|
app/src/content/embeds/wrong-reasons.html
CHANGED
|
@@ -22,12 +22,12 @@
|
|
| 22 |
background: oklch(from var(--danger-color) calc(l + 0.4) c h / 0.26);
|
| 23 |
border: 1px solid oklch(from var(--danger-color) calc(l + 0.15) c h / 0.5);
|
| 24 |
border-radius: 16px;
|
| 25 |
-
padding: var(--spacing-
|
| 26 |
-
text-align:
|
| 27 |
display: flex;
|
| 28 |
flex-direction: column;
|
| 29 |
justify-content: center;
|
| 30 |
-
align-items:
|
| 31 |
min-height: 140px;
|
| 32 |
}
|
| 33 |
|
|
@@ -41,7 +41,7 @@
|
|
| 41 |
font-size: 18px;
|
| 42 |
font-weight: 600;
|
| 43 |
color: var(--danger-color);
|
| 44 |
-
margin-bottom:
|
| 45 |
line-height: 1.2;
|
| 46 |
letter-spacing: -0.01em;
|
| 47 |
position: relative;
|
|
@@ -55,20 +55,41 @@
|
|
| 55 |
|
| 56 |
.wrong-reasons .reason-explanation {
|
| 57 |
font-size: 13px;
|
| 58 |
-
color: oklch(from var(--danger-color) calc(l + 0.05) calc(c * 0.
|
| 59 |
line-height: 1.4;
|
| 60 |
font-weight: 500;
|
| 61 |
-
opacity: 0.
|
| 62 |
position: relative;
|
| 63 |
z-index: 1;
|
| 64 |
font-style: italic;
|
| 65 |
letter-spacing: -0.005em;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}
|
| 67 |
|
| 68 |
/* Dark mode adjustments for better readability */
|
| 69 |
[data-theme="dark"] .wrong-reasons .reason-explanation {
|
| 70 |
-
color: oklch(from var(--danger-color) calc(l + 0.
|
| 71 |
-
opacity: 0.
|
| 72 |
}
|
| 73 |
|
| 74 |
@media (max-width: 768px) {
|
|
|
|
| 22 |
background: oklch(from var(--danger-color) calc(l + 0.4) c h / 0.26);
|
| 23 |
border: 1px solid oklch(from var(--danger-color) calc(l + 0.15) c h / 0.5);
|
| 24 |
border-radius: 16px;
|
| 25 |
+
padding: var(--spacing-6) var(--spacing-8);
|
| 26 |
+
text-align: left;
|
| 27 |
display: flex;
|
| 28 |
flex-direction: column;
|
| 29 |
justify-content: center;
|
| 30 |
+
align-items: flex-start;
|
| 31 |
min-height: 140px;
|
| 32 |
}
|
| 33 |
|
|
|
|
| 41 |
font-size: 18px;
|
| 42 |
font-weight: 600;
|
| 43 |
color: var(--danger-color);
|
| 44 |
+
margin-bottom: var(--spacing-2);
|
| 45 |
line-height: 1.2;
|
| 46 |
letter-spacing: -0.01em;
|
| 47 |
position: relative;
|
|
|
|
| 55 |
|
| 56 |
.wrong-reasons .reason-explanation {
|
| 57 |
font-size: 13px;
|
| 58 |
+
color: oklch(from var(--danger-color) calc(l + 0.05) calc(c * 0.8) h);
|
| 59 |
line-height: 1.4;
|
| 60 |
font-weight: 500;
|
| 61 |
+
opacity: 0.9;
|
| 62 |
position: relative;
|
| 63 |
z-index: 1;
|
| 64 |
font-style: italic;
|
| 65 |
letter-spacing: -0.005em;
|
| 66 |
+
padding-left: 1.6em;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.wrong-reasons .reason-explanation::before {
|
| 70 |
+
content: "";
|
| 71 |
+
position: absolute;
|
| 72 |
+
left: 0;
|
| 73 |
+
top: 0.5em;
|
| 74 |
+
width: 1.4em;
|
| 75 |
+
height: 1.4em;
|
| 76 |
+
background-color: var(--danger-color);
|
| 77 |
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 16'%3E%3Cpath d='M2 2 L2 8 L18 8 M18 8 L14 4 M18 8 L14 12' stroke='white' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
| 78 |
+
mask-size: contain;
|
| 79 |
+
mask-repeat: no-repeat;
|
| 80 |
+
mask-position: center;
|
| 81 |
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 16'%3E%3Cpath d='M2 2 L2 8 L18 8 M18 8 L14 4 M18 8 L14 12' stroke='white' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
| 82 |
+
-webkit-mask-size: contain;
|
| 83 |
+
-webkit-mask-repeat: no-repeat;
|
| 84 |
+
-webkit-mask-position: center;
|
| 85 |
+
opacity: 0.85;
|
| 86 |
+
transform: translateY(-50%);
|
| 87 |
}
|
| 88 |
|
| 89 |
/* Dark mode adjustments for better readability */
|
| 90 |
[data-theme="dark"] .wrong-reasons .reason-explanation {
|
| 91 |
+
color: oklch(from var(--danger-color) calc(l + 0.2) calc(c * 0.95) h);
|
| 92 |
+
opacity: 0.95;
|
| 93 |
}
|
| 94 |
|
| 95 |
@media (max-width: 768px) {
|