+
+
+ {/* Mini-map / Overview Map */}
+
);
}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 032b4f1aa6fbd023f4bcb599365553dd1fa8ddc7..e7c8918373e2bc1d85fc965431a1e987af8f2fdb 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,18 +1,30 @@
-body {
+html, body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+ background: var(--bg-primary, #0d1117);
+ overflow-x: hidden;
+}
+
+body {
+ font-family: 'Overpass', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
- color: #1a1a1a;
- background: #ffffff;
+ color: var(--text-primary, #1a1a1a);
+ transition: background-color var(--transition-base, 200ms), color var(--transition-base, 200ms);
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
+ font-family: 'Roboto Mono', 'Monaco', 'Menlo', 'Courier New', Consolas, monospace;
}
* {
box-sizing: border-box;
}
+#root {
+ min-height: 100vh;
+ background: var(--bg-primary, #0d1117);
+}
+
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index 6fdec74eb653c9e83d76ae1de1f2e2525f275012..358e2dcc28b6c5bad86ef7e8ddc6d3ae43800536 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -26,13 +26,18 @@ const isWebGLError = (error: any, message?: string): boolean => {
const errorStr = error?.toString() || '';
const messageStr = message?.toString() || '';
const combined = `${errorStr} ${messageStr}`.toLowerCase();
+ const stack = error?.stack?.toLowerCase() || '';
return (
combined.includes('webgl') ||
combined.includes('context lost') ||
combined.includes('webglrenderer') ||
+ combined.includes('three.module.js') ||
+ combined.includes('three.js') ||
error?.message?.toLowerCase().includes('webgl') ||
- error?.stack?.toLowerCase().includes('webgl')
+ stack.includes('webgl') ||
+ stack.includes('three.module.js') ||
+ stack.includes('three.js')
);
};
@@ -52,6 +57,18 @@ window.addEventListener('error', (event) => {
event.stopPropagation();
return false;
}
+ // Suppress 404 errors for expected API endpoints
+ if (
+ event.message &&
+ (event.message.includes('404') || event.message.includes('Failed to load resource')) &&
+ (event.message.includes('/api/family/') ||
+ (event.message.includes('/api/model/') && event.message.includes('/papers')) ||
+ event.message.includes('/api/family/path/'))
+ ) {
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
}, true); // Use capture phase to catch early
// Handle unhandled promise rejections related to WebSockets and WebGL
@@ -69,31 +86,102 @@ window.addEventListener('unhandledrejection', (event) => {
event.preventDefault();
event.stopPropagation();
}
+ // Suppress 404 promise rejections for expected API endpoints
+ const reasonStr = String(event.reason || event.promise || '');
+ if (
+ reasonStr.includes('404') &&
+ (reasonStr.includes('/api/family/') ||
+ (reasonStr.includes('/api/model/') && reasonStr.includes('/papers')) ||
+ reasonStr.includes('/api/family/path/'))
+ ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
}, true); // Use capture phase
-// Also suppress console errors for WebSocket and WebGL issues
+// Suppress console errors and warnings for WebSocket and WebGL issues
+// Must override BEFORE any imports that might log
const originalConsoleError = console.error;
-console.error = (...args: any[]) => {
+const originalConsoleWarn = console.warn;
+const originalConsoleLog = console.log;
+
+// Comprehensive error suppression
+const shouldSuppress = (args: any[]): boolean => {
const message = args.join(' ').toLowerCase();
+ const source = args.find(arg => typeof arg === 'string' && arg.includes('.js'));
+
+ // Check for deprecated MouseEvent warnings (from Three.js OrbitControls)
+ if (
+ message.includes('mouseevent.mozpressure') ||
+ message.includes('mouseevent.mozinputsource') ||
+ message.includes('is deprecated')
+ ) {
+ return true;
+ }
+
+ // Check for WebSocket errors
if (
message.includes('websocket') ||
message.includes('websocketclient') ||
message.includes('initsocket')
) {
- // Suppress WebSocket console errors
- return;
+ return true;
}
+
+ // Check for WebGL/Three.js errors (including three.module.js and bundle.js)
if (
message.includes('webgl') ||
message.includes('context lost') ||
- message.includes('webglrenderer')
+ message.includes('webglrenderer') ||
+ message.includes('three.webglrenderer') ||
+ message.includes('three.webglrenderer: context lost') ||
+ (source && (source.includes('three.module.js') ||
+ source.includes('three.js') ||
+ source.includes('bundle.js')))
) {
- // Suppress WebGL context loss console errors (handled by component)
- return;
+ return true;
+ }
+
+ // Check for NetworkError (expected during startup)
+ if (message.includes('networkerror') || message.includes('network error')) {
+ return true;
+ }
+
+ // Suppress 404 errors for expected API endpoints (family, papers, path)
+ if (
+ message.includes('404') &&
+ (message.includes('/api/family/') ||
+ (message.includes('/api/model/') && message.includes('/papers')) ||
+ message.includes('/api/family/path/'))
+ ) {
+ return true;
+ }
+
+ return false;
+};
+
+console.error = (...args: any[]) => {
+ if (shouldSuppress(args)) {
+ return; // Suppress
}
originalConsoleError.apply(console, args);
};
+console.warn = (...args: any[]) => {
+ if (shouldSuppress(args)) {
+ return; // Suppress
+ }
+ originalConsoleWarn.apply(console, args);
+};
+
+// Also suppress console.log for Three.js WebGL messages
+console.log = (...args: any[]) => {
+ if (shouldSuppress(args)) {
+ return; // Suppress
+ }
+ originalConsoleLog.apply(console, args);
+};
+
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
diff --git a/frontend/src/pages/AnalyticsPage.css b/frontend/src/pages/AnalyticsPage.css
new file mode 100644
index 0000000000000000000000000000000000000000..e651838c89344a66f8cb25b3f9c4e0a491d5b956
--- /dev/null
+++ b/frontend/src/pages/AnalyticsPage.css
@@ -0,0 +1,198 @@
+.analytics-page {
+ padding: 2rem;
+ max-width: none;
+ margin: 0;
+ min-height: calc(100vh - 200px);
+ box-sizing: border-box;
+ width: 100%;
+}
+
+.page-header {
+ margin-bottom: 2rem;
+}
+
+.page-header h1 {
+ font-size: 2rem;
+ font-weight: 600;
+ color: var(--text-primary, #1a1a1a);
+ margin: 0;
+ letter-spacing: -0.01em;
+}
+
+.analytics-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(500px, 100%), 1fr));
+ gap: 1.5rem;
+ width: 100%;
+}
+
+.analytics-card {
+ background: var(--bg-primary, #ffffff);
+ border: 1px solid var(--border-light, #e0e0e0);
+ border-radius: 0;
+ overflow: hidden;
+ transition: all var(--transition-base, 0.2s ease);
+}
+
+.analytics-card:hover {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.analytics-card.expanded {
+ grid-column: span 1;
+}
+
+.card-expanded {
+ padding: 1.5rem;
+ overflow-x: visible;
+ width: 100%;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.card-header h3 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--text-primary, #1a1a1a);
+ margin: 0;
+}
+
+.time-tabs {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.tab {
+ padding: 0.375rem 0.75rem;
+ background: var(--bg-secondary, #f5f5f5);
+ border: 1px solid var(--border-medium, #ddd);
+ border-radius: 0;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all var(--transition-base, 0.2s);
+ color: var(--text-secondary, #666);
+ font-family: var(--font-primary, inherit);
+}
+
+.tab:hover {
+ background: var(--bg-tertiary, #e8e8e8);
+}
+
+.tab.active {
+ background: var(--accent-blue, #3b82f6);
+ color: #ffffff;
+ border-color: var(--accent-blue, #3b82f6);
+}
+
+.analytics-table {
+ width: 100%;
+ border-collapse: collapse;
+ table-layout: auto;
+ min-width: 100%;
+ max-width: 100%;
+}
+
+.analytics-table thead {
+ background: var(--bg-secondary, #f5f5f5);
+}
+
+.analytics-table th {
+ padding: 0.75rem;
+ text-align: left;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--text-secondary, #666);
+ border-bottom: 1px solid var(--border-light, #e0e0e0);
+ position: sticky;
+ top: 0;
+ background: var(--bg-secondary, #f5f5f5);
+ z-index: 1;
+}
+
+.analytics-table th:first-child {
+ text-align: center;
+ width: 60px;
+}
+
+.analytics-table th:last-child {
+ text-align: right;
+}
+
+.analytics-table td {
+ padding: 0.75rem;
+ border-bottom: 1px solid var(--border-light, #e0e0e0);
+ font-size: 0.875rem;
+ color: var(--text-primary, #1a1a1a);
+ vertical-align: middle;
+}
+
+.analytics-table td:first-child {
+ width: 60px;
+ text-align: center;
+ font-weight: 600;
+ color: var(--text-secondary, #666);
+}
+
+.analytics-table td:nth-child(2) {
+ max-width: none;
+ overflow: visible;
+ text-overflow: clip;
+ white-space: normal;
+ word-break: break-word;
+ min-width: 200px;
+}
+
+.analytics-table td:last-child {
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+}
+
+.analytics-table tbody tr {
+ cursor: pointer;
+ transition: background-color var(--transition-base, 0.2s);
+}
+
+.analytics-table tbody tr:hover {
+ background: var(--bg-secondary, #f5f5f5);
+}
+
+.analytics-table tbody tr:last-child td {
+ border-bottom: none;
+}
+
+.placeholder {
+ text-align: center;
+ color: var(--text-secondary, #666);
+ font-style: italic;
+ padding: 2rem;
+}
+
+.card-placeholder {
+ padding: 2rem;
+ text-align: center;
+ color: var(--text-secondary, #666);
+ font-style: italic;
+}
+
+@media (max-width: 768px) {
+ .analytics-page {
+ padding: 1rem;
+ }
+
+ .analytics-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .card-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+}
diff --git a/frontend/src/pages/AnalyticsPage.tsx b/frontend/src/pages/AnalyticsPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e9eae332e152a14e8e71b2af155be85137abd7a7
--- /dev/null
+++ b/frontend/src/pages/AnalyticsPage.tsx
@@ -0,0 +1,350 @@
+import React, { useState, useEffect } from 'react';
+import { API_BASE } from '../config/api';
+import LoadingProgress from '../components/ui/LoadingProgress';
+import './AnalyticsPage.css';
+
+interface TopModel {
+ model_id: string;
+ downloads?: number;
+ likes?: number;
+ trending_score?: number;
+ created_at?: string;
+}
+
+interface Family {
+ family: string;
+ count: number;
+ growth_rate?: number;
+}
+
+export default function AnalyticsPage() {
+ const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d'>('30d');
+ const [loading, setLoading] = useState(true);
+ const [loadingProgress, setLoadingProgress] = useState(0);
+
+ const [topDownloads, setTopDownloads] = useState
([]);
+ const [topLikes, setTopLikes] = useState([]);
+ const [trending, setTrending] = useState([]);
+ const [newest, setNewest] = useState([]);
+ const [largestFamilies, setLargestFamilies] = useState([]);
+ const [fastestGrowing, setFastestGrowing] = useState([]);
+
+ useEffect(() => {
+ const fetchAnalytics = async () => {
+ setLoading(true);
+ setLoadingProgress(0);
+
+ try {
+ // Fetch models data and sort by different criteria
+ setLoadingProgress(20);
+ const response = await fetch(`${API_BASE}/api/models?max_points=10000&format=json`);
+ if (!response.ok) throw new Error('Failed to fetch models');
+
+ setLoadingProgress(40);
+ const data = await response.json();
+ const models: TopModel[] = Array.isArray(data) ? data : (data.models || []);
+
+ // Sort by downloads
+ setLoadingProgress(50);
+ const sortedByDownloads = [...models]
+ .sort((a, b) => (b.downloads || 0) - (a.downloads || 0))
+ .slice(0, 20);
+ setTopDownloads(sortedByDownloads);
+
+ // Sort by likes
+ setLoadingProgress(60);
+ const sortedByLikes = [...models]
+ .sort((a, b) => (b.likes || 0) - (a.likes || 0))
+ .slice(0, 20);
+ setTopLikes(sortedByLikes);
+
+ // Sort by trending score
+ setLoadingProgress(70);
+ const sortedByTrending = [...models]
+ .filter(m => m.trending_score !== null && m.trending_score !== undefined)
+ .sort((a, b) => (b.trending_score || 0) - (a.trending_score || 0))
+ .slice(0, 20);
+ setTrending(sortedByTrending);
+
+ // Sort by created_at (newest)
+ setLoadingProgress(80);
+ const sortedByNewest = [...models]
+ .filter(m => m.created_at)
+ .sort((a, b) => {
+ const dateA = new Date(a.created_at || 0).getTime();
+ const dateB = new Date(b.created_at || 0).getTime();
+ return dateB - dateA;
+ })
+ .slice(0, 20);
+ setNewest(sortedByNewest);
+
+ // Group by family (using parent_model or model_id prefix)
+ setLoadingProgress(90);
+ const familyMap = new Map();
+ models.forEach(model => {
+ // Extract family name from model_id (e.g., "meta-llama/Meta-Llama-3" -> "meta-llama")
+ const family = model.model_id.split('/')[0];
+ familyMap.set(family, (familyMap.get(family) || 0) + 1);
+ });
+
+ const families: Family[] = Array.from(familyMap.entries())
+ .map(([family, count]) => ({ family, count }))
+ .sort((a, b) => b.count - a.count)
+ .slice(0, 20);
+ setLargestFamilies(families);
+ setFastestGrowing(families); // TODO: Calculate actual growth rate
+
+ setLoadingProgress(100);
+ setLoading(false);
+ } catch {
+ setLoading(false);
+ }
+ };
+
+ fetchAnalytics();
+ }, []);
+
+ const renderCardContent = (cardType: string) => {
+ switch (cardType) {
+ case 'downloads':
+ return (
+
+
+
Top Downloads ({timeRange})
+
+
+
+
+
+
+
+
+
+ | Rank |
+ Model |
+ Downloads |
+
+
+
+ {topDownloads.length > 0 ? (
+ topDownloads.map((model, idx) => (
+
+ | {idx + 1} |
+ {model.model_id} |
+ {model.downloads?.toLocaleString() || 'N/A'} |
+
+ ))
+ ) : (
+ | Loading... |
+ )}
+
+
+
+ );
+ case 'likes':
+ return (
+
+
Top Likes
+
+
+
+ | Rank |
+ Model |
+ Likes |
+
+
+
+ {topLikes.length > 0 ? (
+ topLikes.map((model, idx) => (
+
+ | {idx + 1} |
+ {model.model_id} |
+ {model.likes?.toLocaleString() || 'N/A'} |
+
+ ))
+ ) : (
+ | Loading... |
+ )}
+
+
+
+ );
+ case 'trending':
+ return (
+
+
Trending Models
+
+
+
+ | Rank |
+ Model |
+ Trending Score |
+
+
+
+ {trending.length > 0 ? (
+ trending.map((model, idx) => (
+
+ | {idx + 1} |
+ {model.model_id} |
+ {model.trending_score?.toFixed(2) || 'N/A'} |
+
+ ))
+ ) : (
+ | Loading... |
+ )}
+
+
+
+ );
+ case 'newest':
+ return (
+
+
Newest Models
+
+
+
+ | Rank |
+ Model |
+ Created |
+
+
+
+ {newest.length > 0 ? (
+ newest.map((model, idx) => (
+
+ | {idx + 1} |
+ {model.model_id} |
+ {model.created_at ? new Date(model.created_at).toLocaleDateString() : 'N/A'} |
+
+ ))
+ ) : (
+ | Loading... |
+ )}
+
+
+
+ );
+ case 'largest':
+ return (
+
+
Largest Families
+
+
+
+ | Rank |
+ Family |
+ Model Count |
+
+
+
+ {largestFamilies.length > 0 ? (
+ largestFamilies.map((family, idx) => (
+
+ | {idx + 1} |
+ {family.family} |
+ {family.count.toLocaleString()} |
+
+ ))
+ ) : (
+ | Loading... |
+ )}
+
+
+
+ );
+ case 'fastest':
+ return (
+
+
Fastest-Growing Families
+
+
+
+ | Rank |
+ Family |
+ Model Count |
+
+
+
+ {fastestGrowing.length > 0 ? (
+ fastestGrowing.map((family, idx) => (
+
+ | {idx + 1} |
+ {family.family} |
+ {family.count.toLocaleString()} |
+
+ ))
+ ) : (
+ | Loading... |
+ )}
+
+
+
+ );
+ default:
+ return Content coming soon
;
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Analytics
+
+
+
+
+ {renderCardContent('downloads')}
+
+
+
+ {renderCardContent('likes')}
+
+
+
+ {renderCardContent('trending')}
+
+
+
+ {renderCardContent('newest')}
+
+
+
+ {renderCardContent('largest')}
+
+
+
+ {renderCardContent('fastest')}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/FamiliesPage.css b/frontend/src/pages/FamiliesPage.css
new file mode 100644
index 0000000000000000000000000000000000000000..e3b94de3375e3a7ff4446a312b1b67c91a87550b
--- /dev/null
+++ b/frontend/src/pages/FamiliesPage.css
@@ -0,0 +1,300 @@
+.families-page {
+ padding: 2rem;
+ max-width: 1400px;
+ margin: 0 auto;
+ min-height: calc(100vh - 200px);
+}
+
+.page-header {
+ margin-bottom: 2rem;
+}
+
+.page-header h1 {
+ font-size: 2rem;
+ font-weight: 600;
+ color: var(--text-primary, #1a1a1a);
+ margin: 0 0 0.5rem 0;
+ letter-spacing: -0.01em;
+}
+
+.page-description {
+ font-size: 0.9rem;
+ color: var(--text-secondary, #666);
+ margin: 0;
+ line-height: 1.5;
+}
+
+.families-section {
+ margin-top: 2rem;
+}
+
+.families-section h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 1.5rem;
+ color: var(--text-primary, #1a1a1a);
+}
+
+.families-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.family-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.25rem;
+ background: var(--bg-primary, #ffffff);
+ border: 1px solid var(--border-light, #e0e0e0);
+ border-radius: 0;
+ transition: border-color var(--transition-base, 0.2s ease),
+ background-color var(--transition-base, 0.2s ease),
+ box-shadow var(--transition-base, 0.2s ease);
+ cursor: pointer;
+ user-select: none;
+}
+
+.family-item:hover:not(.selected) {
+ border-color: var(--accent-blue, #3b82f6);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.family-item:active {
+ opacity: 0.9;
+}
+
+.family-info {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ flex: 1;
+}
+
+.rank {
+ font-weight: 600;
+ color: var(--text-secondary, #666);
+ min-width: 2rem;
+ font-size: 1.1rem;
+}
+
+.family-details {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ flex: 1;
+}
+
+.family-name {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-primary, #1a1a1a);
+}
+
+.family-count {
+ font-size: 0.9rem;
+ color: var(--text-secondary, #666);
+}
+
+.family-count .tree-count {
+ font-size: 0.8rem;
+ color: var(--text-tertiary, #999);
+ font-weight: 400;
+}
+
+.family-stats {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.stat-badge {
+ padding: 0.5rem 1rem;
+ background: var(--bg-secondary, #f5f5f5);
+ border: 1px solid var(--border-medium, #ddd);
+ border-radius: 0;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--text-primary, #1a1a1a);
+ min-width: 60px;
+ text-align: center;
+}
+
+.family-item.selected {
+ border-color: var(--accent-blue, #3b82f6);
+ background: rgba(59, 130, 246, 0.05);
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
+}
+
+.family-item.selected:hover {
+ background: rgba(59, 130, 246, 0.08);
+}
+
+.adoption-section {
+ margin-bottom: 3rem;
+ padding: 2rem;
+ background: var(--bg-primary, #ffffff);
+ border: 1px solid var(--border-light, #e0e0e0);
+ border-radius: 0;
+ animation: slideDown 0.3s ease-out;
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.adoption-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+}
+
+.adoption-controls {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.comparison-toggle-btn {
+ padding: 0.5rem 1rem;
+ background: var(--bg-primary, #ffffff);
+ border: 1px solid var(--border-medium, #ddd);
+ border-radius: 0;
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--text-primary, #1a1a1a);
+ cursor: pointer;
+ transition: all var(--transition-base, 0.2s ease);
+}
+
+.comparison-toggle-btn:hover {
+ background: var(--bg-secondary, #f5f5f5);
+ border-color: var(--accent-blue, #3b82f6);
+ color: var(--accent-blue, #3b82f6);
+}
+
+.family-selection {
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ background: var(--bg-secondary, #f5f5f5);
+ border: 1px solid var(--border-light, #e0e0e0);
+ border-radius: 0;
+}
+
+.selection-label {
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: var(--text-primary, #1a1a1a);
+ margin: 0 0 0.75rem 0;
+}
+
+.family-checkboxes {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.family-checkbox {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ color: var(--text-primary, #1a1a1a);
+}
+
+.family-checkbox input[type="checkbox"] {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+}
+
+.family-checkbox:hover {
+ color: var(--accent-blue, #3b82f6);
+}
+
+.adoption-header h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin: 0;
+ color: var(--text-primary, #1a1a1a);
+}
+
+.close-button {
+ background: transparent;
+ border: 1px solid var(--border-medium, #ddd);
+ border-radius: 0;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 1.5rem;
+ color: var(--text-secondary, #666);
+ transition: all var(--transition-base, 0.2s ease);
+}
+
+.close-button:hover {
+ background: var(--bg-secondary, #f5f5f5);
+ border-color: var(--accent-blue, #3b82f6);
+ color: var(--text-primary, #1a1a1a);
+}
+
+.adoption-curve-wrapper {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.chart-info {
+ margin-bottom: 0.5rem;
+}
+
+.chart-subtitle {
+ font-size: 0.9rem;
+ color: var(--text-secondary, #666);
+ margin: 0;
+}
+
+.adoption-empty {
+ padding: 3rem;
+ text-align: center;
+ color: var(--text-secondary, #666);
+ background: var(--bg-secondary, #f5f5f5);
+ border: 1px solid var(--border-light, #e0e0e0);
+ border-radius: 0;
+}
+
+@media (max-width: 768px) {
+ .families-page {
+ padding: 1rem;
+ }
+
+ .family-item {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ }
+
+ .family-stats {
+ width: 100%;
+ justify-content: flex-end;
+ }
+
+ .adoption-section {
+ padding: 1rem;
+ }
+
+ .adoption-header h2 {
+ font-size: 1.25rem;
+ }
+}
diff --git a/frontend/src/pages/FamiliesPage.tsx b/frontend/src/pages/FamiliesPage.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..42e4b8b854f00103cdd32785c2695df44fafee8e
--- /dev/null
+++ b/frontend/src/pages/FamiliesPage.tsx
@@ -0,0 +1,397 @@
+import React, { useState, useEffect } from 'react';
+import { API_BASE } from '../config/api';
+import LoadingProgress from '../components/ui/LoadingProgress';
+import AdoptionCurve, { AdoptionDataPoint } from '../components/visualizations/AdoptionCurve';
+import './FamiliesPage.css';
+
+interface Family {
+ family: string;
+ count: number;
+ root_model?: string;
+ family_count?: number; // Number of separate family trees
+ root_models?: string[]; // List of root models for this org
+}
+
+interface AdoptionModel {
+ model_id: string;
+ downloads: number;
+ created_at: string;
+}
+
+interface FamilyAdoptionData {
+ family: string;
+ data: AdoptionDataPoint[];
+ color: string;
+}
+
+export default function FamiliesPage() {
+ const [families, setFamilies] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [loadingProgress, setLoadingProgress] = useState(0);
+ const [selectedFamily, setSelectedFamily] = useState(null);
+ const [adoptionData, setAdoptionData] = useState([]);
+ const [loadingAdoption, setLoadingAdoption] = useState(false);
+ const [selectedModel, setSelectedModel] = useState(undefined);
+
+ // Comparison mode - enabled by default
+ const [comparisonMode, setComparisonMode] = useState(true);
+ const [selectedFamiliesForComparison, setSelectedFamiliesForComparison] = useState>(new Set());
+ const [familyAdoptionData, setFamilyAdoptionData] = useState([]);
+ const [loadingComparison, setLoadingComparison] = useState(false);
+
+ useEffect(() => {
+ const fetchFamilies = async () => {
+ setLoading(true);
+ setLoadingProgress(0);
+
+ try {
+ setLoadingProgress(20);
+
+ // Fetch models data to count by organization
+ const response = await fetch(`${API_BASE}/api/models?max_points=10000&format=json`);
+ if (!response.ok) throw new Error('Failed to fetch models');
+
+ setLoadingProgress(40);
+ const data = await response.json();
+ const models = Array.isArray(data) ? data : (data.models || []);
+
+ setLoadingProgress(60);
+
+ // Group by organization (model_id prefix)
+ // Also track models by family_depth to show lineage distribution
+ const familyMap = new Map }>();
+
+ models.forEach((model: any) => {
+ const org = model.model_id?.split('/')[0] || 'unknown';
+ const depth = model.family_depth ?? 0;
+
+ if (!familyMap.has(org)) {
+ familyMap.set(org, { total: 0, byDepth: new Map() });
+ }
+
+ const orgData = familyMap.get(org)!;
+ orgData.total += 1;
+ orgData.byDepth.set(depth, (orgData.byDepth.get(depth) || 0) + 1);
+ });
+
+ setLoadingProgress(80);
+
+ // Convert to array and calculate depth distribution info
+ const familiesList: Family[] = Array.from(familyMap.entries())
+ .map(([family, data]) => {
+ // Count unique depths to show family tree complexity
+ const depthCount = data.byDepth.size;
+ const maxDepth = Math.max(...Array.from(data.byDepth.keys()));
+
+ return {
+ family,
+ count: data.total,
+ family_count: depthCount > 1 ? depthCount : undefined, // Number of depth levels
+ root_models: maxDepth > 0 ? [`max depth: ${maxDepth}`] : undefined
+ };
+ })
+ .sort((a, b) => b.count - a.count)
+ .slice(0, 50);
+
+ setFamilies(familiesList);
+
+ // Initialize comparison mode with top 5 families (if not already set)
+ if (familiesList.length >= 5 && selectedFamiliesForComparison.size === 0) {
+ const top5 = familiesList.slice(0, 5).map(f => f.family);
+ setSelectedFamiliesForComparison(new Set(top5));
+ }
+
+ setLoadingProgress(100);
+ setLoading(false);
+ } catch {
+ setLoading(false);
+ }
+ };
+
+ fetchFamilies();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // Fetch adoption data when family is selected
+ useEffect(() => {
+ if (!selectedFamily) {
+ setAdoptionData([]);
+ return;
+ }
+
+ const fetchAdoptionData = async () => {
+ setLoadingAdoption(true);
+ try {
+ const response = await fetch(`${API_BASE}/api/family/adoption?family=${encodeURIComponent(selectedFamily)}&limit=200`);
+ if (!response.ok) throw new Error('Failed to fetch adoption data');
+
+ const data = await response.json();
+ const models: AdoptionModel[] = data.models || [];
+
+ // Transform to AdoptionDataPoint format
+ const chartData: AdoptionDataPoint[] = models
+ .filter((m) => m.created_at)
+ .map((m) => ({
+ date: new Date(m.created_at),
+ downloads: m.downloads,
+ modelId: m.model_id,
+ }))
+ .sort((a, b) => a.date.getTime() - b.date.getTime());
+
+ setAdoptionData(chartData);
+
+ // Select the model with highest downloads by default
+ if (chartData.length > 0) {
+ const topModel = chartData.reduce((max, current) =>
+ current.downloads > max.downloads ? current : max
+ );
+ setSelectedModel(topModel.modelId);
+ }
+ } catch {
+ setAdoptionData([]);
+ } finally {
+ setLoadingAdoption(false);
+ }
+ };
+
+ fetchAdoptionData();
+ }, [selectedFamily]);
+
+ // Fetch adoption data for comparison mode
+ useEffect(() => {
+ if (!comparisonMode || selectedFamiliesForComparison.size === 0) {
+ setFamilyAdoptionData([]);
+ return;
+ }
+
+ const fetchComparisonData = async () => {
+ setLoadingComparison(true);
+ try {
+ const familyNames = Array.from(selectedFamiliesForComparison);
+ const FAMILY_COLORS = [
+ '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
+ '#ec4899', '#06b6d4', '#f97316'
+ ];
+
+ const adoptionPromises = familyNames.map(async (familyName, idx): Promise => {
+ try {
+ const response = await fetch(`${API_BASE}/api/family/adoption?family=${encodeURIComponent(familyName)}&limit=200`);
+ if (!response.ok) throw new Error(`Failed to fetch adoption data for ${familyName}`);
+
+ const data = await response.json();
+ const models: AdoptionModel[] = data.models || [];
+
+ const chartData: AdoptionDataPoint[] = models
+ .filter((m) => m.created_at)
+ .map((m) => ({
+ date: new Date(m.created_at),
+ downloads: m.downloads,
+ modelId: m.model_id,
+ }))
+ .sort((a, b) => a.date.getTime() - b.date.getTime());
+
+ return {
+ family: familyName,
+ data: chartData,
+ color: FAMILY_COLORS[idx % FAMILY_COLORS.length],
+ };
+ } catch {
+ return null;
+ }
+ });
+
+ const results = await Promise.all(adoptionPromises);
+ const validResults = results.filter((r): r is FamilyAdoptionData => r !== null);
+ setFamilyAdoptionData(validResults);
+ } catch {
+ setFamilyAdoptionData([]);
+ } finally {
+ setLoadingComparison(false);
+ }
+ };
+
+ fetchComparisonData();
+ }, [comparisonMode, selectedFamiliesForComparison]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Model Families
+
+ Explore the largest model families on Hugging Face, organized by organization and model lineage.
+
+
+
+ {/* Adoption Curve Section - Always visible */}
+
+
+
+ Adoption Curve
+
+
+
+
+
+
+ {comparisonMode ? (
+ <>
+ {/* Family selection checkboxes */}
+
+
Select families to compare:
+
+ {families.slice(0, 10).map((family) => (
+
+ ))}
+
+
+
+ {loadingComparison ? (
+
+ ) : familyAdoptionData.length > 0 ? (
+
+
+
+ {families
+ .filter(f => selectedFamiliesForComparison.has(f.family))
+ .reduce((sum, f) => sum + f.count, 0)
+ .toLocaleString()} models across selected organizations • Cumulative downloads over time
+
+
+
+
+ ) : (
+
+
No adoption data available for selected families.
+
+ )}
+ >
+ ) : (
+ <>
+ {loadingAdoption ? (
+
+ ) : adoptionData.length > 0 ? (
+
+
+
+ {adoptionData.length} models • Cumulative downloads over time
+
+
+
+
+ ) : (
+
+
No adoption data available for this family.
+
+ )}
+ >
+ )}
+
+
+
+ Top Families by Model Count
+
+ {families.map((family, idx) => (
+
{
+ if (selectedFamily === family.family) {
+ setSelectedFamily(null);
+ setAdoptionData([]);
+ setSelectedModel(undefined);
+ } else {
+ setSelectedFamily(family.family);
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ if (selectedFamily === family.family) {
+ setSelectedFamily(null);
+ setAdoptionData([]);
+ setSelectedModel(undefined);
+ } else {
+ setSelectedFamily(family.family);
+ }
+ }
+ }}
+ >
+
+
{idx + 1}.
+
+ {family.family}
+
+ {family.count.toLocaleString()} models
+
+
+
+
+
+ {((family.count / families.reduce((sum, f) => sum + f.count, 0)) * 100).toFixed(1)}%
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/src/stores/filterStore.ts b/frontend/src/stores/filterStore.ts
index 176c48edac74c26e79cd0c6a23a05446b664fbba..8fe9d002089b400bee158e78f98d8a62aeaa90c5 100644
--- a/frontend/src/stores/filterStore.ts
+++ b/frontend/src/stores/filterStore.ts
@@ -6,11 +6,11 @@ import { create } from 'zustand';
export type ColorByOption = 'domain' | 'license' | 'family' | 'library' | 'library_name' | 'pipeline_tag' | 'cluster_id' | 'downloads' | 'likes' | 'family_depth' | 'trending_score' | 'licenses';
export type SizeByOption = 'downloads' | 'likes' | 'none';
-export type ViewMode = 'scatter' | '3d' | 'network' | 'distribution';
+export type ViewMode = '3d' | 'network' | 'distribution';
export type RenderingStyle = 'embeddings' | 'sphere' | 'galaxy' | 'wave' | 'helix' | 'torus';
export type Theme = 'light' | 'dark';
-interface FilterState {
+export interface FilterState {
// Filters
domains: string[];
licenses: string[];
diff --git a/frontend/src/utils/rendering/colors.ts b/frontend/src/utils/rendering/colors.ts
index b3074dcda8e1095f22a7c486ae54c685875f5a1c..da2e4994136fbbe199c2ea43b302f5d1f1e10db4 100644
--- a/frontend/src/utils/rendering/colors.ts
+++ b/frontend/src/utils/rendering/colors.ts
@@ -3,115 +3,182 @@
* Supports categorical and continuous color scales.
*/
-// Extended color palettes for better variety - Enhanced vibrancy
+// Extended color palettes - HIGHLY VIBRANT for dark mode visibility
export const CATEGORICAL_COLORS = [
- '#2563eb', '#f59e0b', '#10b981', '#ef4444', '#8b5cf6',
- '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1',
- '#14b8a6', '#a855f7', '#f43f5e', '#0ea5e9', '#22c55e',
- '#eab308', '#3b82f6', '#8b5cf6', '#ec4899', '#06b6d4',
- '#6b6ecf', '#b5cf6b', '#bd9e39', '#e7969c', '#7b4173',
- '#a55194', '#ce6dbd', '#de9ed6', '#636363', '#8ca252',
- '#b5a252', '#d6616b', '#e7ba52', '#ad494a', '#843c39',
- '#d6616b', '#e7969c', '#e7ba52', '#b5cf6b', '#8ca252',
- '#637939', '#bd9e39', '#d6616b', '#e7969c', '#e7ba52',
+ '#60a5fa', '#fbbf24', '#34d399', '#f87171', '#a78bfa', // Bright versions
+ '#f472b6', '#22d3ee', '#a3e635', '#fb923c', '#818cf8',
+ '#2dd4bf', '#c084fc', '#fb7185', '#38bdf8', '#4ade80',
+ '#facc15', '#3b82f6', '#a855f7', '#ec4899', '#06b6d4',
+ '#00ff88', '#ff6b6b', '#4ecdc4', '#ffe66d', '#95e1d3',
+ '#ff9ff3', '#54a0ff', '#5f27cd', '#00d2d3', '#ff9f43',
+ '#ee5a24', '#0abde3', '#10ac84', '#ff6b81', '#7bed9f',
+ '#70a1ff', '#5352ed', '#ff4757', '#2ed573', '#ffa502',
];
-// Color schemes for different features - Enhanced vibrancy
+// Color schemes for different features - EXTRA VIBRANT for dark mode
+// Grouped by semantic meaning: NLP (blues), Vision (greens), Audio (purples), Generative (reds/oranges)
export const LIBRARY_COLORS: Record = {
- 'transformers': '#2563eb',
- 'diffusers': '#f59e0b',
- 'sentence-transformers': '#10b981',
- 'timm': '#ef4444',
- 'speechbrain': '#8b5cf6',
- 'fairseq': '#ec4899',
- 'espnet': '#06b6d4',
- 'asteroid': '#84cc16',
- 'keras': '#f97316',
- 'sklearn': '#6366f1',
- 'unknown': '#9ca3af',
+ // NLP / Text frameworks - Bright Blues and Cyans
+ 'transformers': '#60a5fa', // Bright blue - most common
+ 'sentence-transformers': '#22d3ee', // Bright cyan
+ 'fairseq': '#38bdf8', // Sky blue
+ 'spacy': '#2dd4bf', // Teal
+
+ // Vision frameworks - Bright Greens and Limes
+ 'timm': '#4ade80', // Bright green
+ 'torchvision': '#a3e635', // Bright lime
+ 'mmdet': '#bef264', // Yellow-green
+
+ // Diffusion / Generative - Bright Oranges and Reds
+ 'diffusers': '#fb923c', // Bright orange
+ 'stable-baselines3': '#fdba74', // Light orange
+
+ // Audio frameworks - Bright Purples and Pinks
+ 'speechbrain': '#c084fc', // Bright purple
+ 'espnet': '#e879f9', // Bright fuchsia
+ 'asteroid': '#f472b6', // Bright pink
+
+ // ML frameworks - Bright Warm colors
+ 'keras': '#fbbf24', // Bright amber
+ 'sklearn': '#facc15', // Bright yellow
+ 'pytorch': '#f87171', // Bright red
+
+ // Other
+ 'unknown': '#cbd5e1', // Light slate
};
export const PIPELINE_COLORS: Record = {
- 'text-classification': '#2563eb',
- 'token-classification': '#f59e0b',
- 'question-answering': '#10b981',
- 'summarization': '#ef4444',
- 'translation': '#8b5cf6',
- 'text-generation': '#ec4899',
- 'fill-mask': '#06b6d4',
- 'zero-shot-classification': '#84cc16',
- 'automatic-speech-recognition': '#f97316',
- 'text-to-speech': '#6366f1',
- 'image-classification': '#14b8a6',
- 'object-detection': '#a855f7',
- 'image-segmentation': '#f43f5e',
- 'image-to-text': '#0ea5e9',
- 'text-to-image': '#22c55e',
- 'unknown': '#9ca3af',
+ // Text tasks - Bright Blues
+ 'text-classification': '#60a5fa',
+ 'token-classification': '#93c5fd',
+ 'question-answering': '#38bdf8',
+ 'fill-mask': '#22d3ee',
+ 'text-generation': '#2dd4bf',
+ 'summarization': '#5eead4',
+ 'translation': '#99f6e4',
+ 'zero-shot-classification': '#a5f3fc',
+
+ // Vision tasks - Bright Greens
+ 'image-classification': '#4ade80',
+ 'object-detection': '#86efac',
+ 'image-segmentation': '#bbf7d0',
+ 'image-to-text': '#bef264',
+
+ // Generative tasks - Bright Oranges/Reds
+ 'text-to-image': '#fb923c',
+ 'image-to-image': '#fdba74',
+
+ // Audio tasks - Bright Purples
+ 'automatic-speech-recognition': '#c084fc',
+ 'text-to-speech': '#d8b4fe',
+ 'audio-classification': '#e879f9',
+
+ // Other
+ 'unknown': '#cbd5e1',
};
-// Continuous color scales with optional logarithmic scaling
+// Depth-based color scale - Multi-hue gradient for maximum visibility
+// Root models are bright cyan, deepest are bright magenta
+export function getDepthColorScale(maxDepth: number, isDarkMode: boolean = true): (depth: number) => string {
+ return (depth: number) => {
+ // Normalize depth to 0-1 range
+ const normalized = Math.max(0, Math.min(1, depth / Math.max(maxDepth, 1)));
+
+ if (isDarkMode) {
+ // Dark mode: Use a vibrant multi-hue gradient (cyan -> green -> yellow -> orange -> pink)
+ // This provides maximum distinguishability between depth levels
+ if (normalized < 0.25) {
+ // Cyan to Green
+ const t = normalized * 4;
+ return `rgb(${Math.floor(34 + (74 - 34) * t)}, ${Math.floor(211 + (222 - 211) * t)}, ${Math.floor(238 + (128 - 238) * t)})`;
+ } else if (normalized < 0.5) {
+ // Green to Yellow
+ const t = (normalized - 0.25) * 4;
+ return `rgb(${Math.floor(74 + (250 - 74) * t)}, ${Math.floor(222 + (204 - 222) * t)}, ${Math.floor(128 + (21 - 128) * t)})`;
+ } else if (normalized < 0.75) {
+ // Yellow to Orange
+ const t = (normalized - 0.5) * 4;
+ return `rgb(${Math.floor(250 + (251 - 250) * t)}, ${Math.floor(204 + (146 - 204) * t)}, ${Math.floor(21 + (60 - 21) * t)})`;
+ } else {
+ // Orange to Pink/Magenta
+ const t = (normalized - 0.75) * 4;
+ return `rgb(${Math.floor(251 + (244 - 251) * t)}, ${Math.floor(146 + (114 - 146) * t)}, ${Math.floor(60 + (182 - 60) * t)})`;
+ }
+ } else {
+ // Light mode: Darker, more saturated colors
+ if (normalized < 0.5) {
+ const t = normalized * 2;
+ return `rgb(${Math.floor(30 + (100 - 30) * t)}, ${Math.floor(100 + (50 - 100) * t)}, ${Math.floor(200 + (150 - 200) * t)})`;
+ } else {
+ const t = (normalized - 0.5) * 2;
+ return `rgb(${Math.floor(100 + (150 - 100) * t)}, ${Math.floor(50 + (30 - 50) * t)}, ${Math.floor(150 + (100 - 150) * t)})`;
+ }
+ }
+ };
+}
+
+// Continuous color scales - EXTRA VIBRANT for dark mode visibility
export function getContinuousColorScale(
min: number,
max: number,
scheme: 'viridis' | 'plasma' | 'inferno' | 'magma' | 'coolwarm' = 'viridis',
useLogScale: boolean = false
): (value: number) => string {
- // Use logarithmic scaling for heavily skewed distributions (like downloads/likes)
- // This provides better visual representation of the data distribution
const range = max - min || 1;
const logMin = useLogScale && min > 0 ? Math.log10(min + 1) : min;
const logMax = useLogScale && max > 0 ? Math.log10(max + 1) : max;
const logRange = logMax - logMin || 1;
- // Viridis-like color scale (blue to yellow) - Enhanced vibrancy
+ // Viridis - Bright cyan to bright yellow (enhanced for dark mode)
const viridis = (t: number) => {
- // Apply gamma correction for more vibrant colors
- const gamma = 0.7;
- const tGamma = Math.pow(t, gamma);
- const r = Math.floor(68 + (253 - 68) * tGamma);
- const g = Math.floor(1 + (231 - 1) * tGamma);
- const b = Math.floor(84 + (37 - 84) * tGamma);
- // Increase saturation slightly
- return `rgb(${Math.min(255, r)}, ${Math.min(255, g)}, ${Math.min(255, b)})`;
+ if (t < 0.33) {
+ const s = t * 3;
+ return `rgb(${Math.floor(68 + (32 - 68) * s)}, ${Math.floor(170 + (200 - 170) * s)}, ${Math.floor(220 + (170 - 220) * s)})`;
+ } else if (t < 0.66) {
+ const s = (t - 0.33) * 3;
+ return `rgb(${Math.floor(32 + (120 - 32) * s)}, ${Math.floor(200 + (220 - 200) * s)}, ${Math.floor(170 + (90 - 170) * s)})`;
+ } else {
+ const s = (t - 0.66) * 3;
+ return `rgb(${Math.floor(120 + (253 - 120) * s)}, ${Math.floor(220 + (231 - 220) * s)}, ${Math.floor(90 + (37 - 90) * s)})`;
+ }
};
- // Plasma color scale (purple to yellow) - Enhanced vibrancy
+ // Plasma - Bright purple to bright yellow
const plasma = (t: number) => {
- const gamma = 0.7;
- const tGamma = Math.pow(t, gamma);
- const r = Math.floor(13 + (240 - 13) * tGamma);
- const g = Math.floor(8 + (249 - 8) * tGamma);
- const b = Math.floor(135 + (33 - 135) * tGamma);
- return `rgb(${Math.min(255, r)}, ${Math.min(255, g)}, ${Math.min(255, b)})`;
+ if (t < 0.33) {
+ const s = t * 3;
+ return `rgb(${Math.floor(100 + (180 - 100) * s)}, ${Math.floor(50 + (50 - 50) * s)}, ${Math.floor(200 + (220 - 200) * s)})`;
+ } else if (t < 0.66) {
+ const s = (t - 0.33) * 3;
+ return `rgb(${Math.floor(180 + (240 - 180) * s)}, ${Math.floor(50 + (100 - 50) * s)}, ${Math.floor(220 + (150 - 220) * s)})`;
+ } else {
+ const s = (t - 0.66) * 3;
+ return `rgb(${Math.floor(240 + (255 - 240) * s)}, ${Math.floor(100 + (220 - 100) * s)}, ${Math.floor(150 + (50 - 150) * s)})`;
+ }
};
- // Inferno color scale (black to yellow) - Enhanced vibrancy
+ // Inferno - Dark red to bright yellow
const inferno = (t: number) => {
- const gamma = 0.6;
- const tGamma = Math.pow(t, gamma);
- const r = Math.floor(0 + (252 - 0) * tGamma);
- const g = Math.floor(0 + (141 - 0) * tGamma);
- const b = Math.floor(4 + (89 - 4) * tGamma);
- return `rgb(${Math.min(255, r)}, ${Math.min(255, g)}, ${Math.min(255, b)})`;
+ if (t < 0.33) {
+ const s = t * 3;
+ return `rgb(${Math.floor(60 + (150 - 60) * s)}, ${Math.floor(20 + (40 - 20) * s)}, ${Math.floor(80 + (100 - 80) * s)})`;
+ } else if (t < 0.66) {
+ const s = (t - 0.33) * 3;
+ return `rgb(${Math.floor(150 + (230 - 150) * s)}, ${Math.floor(40 + (100 - 40) * s)}, ${Math.floor(100 + (50 - 100) * s)})`;
+ } else {
+ const s = (t - 0.66) * 3;
+ return `rgb(${Math.floor(230 + (255 - 230) * s)}, ${Math.floor(100 + (200 - 100) * s)}, ${Math.floor(50 + (70 - 50) * s)})`;
+ }
};
- // Cool-warm color scale (blue to red)
+ // Cool-warm - Bright cyan to bright red
const coolwarm = (t: number) => {
if (t < 0.5) {
- // Cool (blue)
const s = t * 2;
- const r = Math.floor(59 * s);
- const g = Math.floor(76 * s);
- const b = Math.floor(192 + (255 - 192) * s);
- return `rgb(${r}, ${g}, ${b})`;
+ return `rgb(${Math.floor(80 + (200 - 80) * s)}, ${Math.floor(180 + (200 - 180) * s)}, ${Math.floor(255 + (220 - 255) * s)})`;
} else {
- // Warm (red)
const s = (t - 0.5) * 2;
- const r = Math.floor(180 + (255 - 180) * s);
- const g = Math.floor(4 + (180 - 4) * s);
- const b = Math.floor(38 * (1 - s));
- return `rgb(${r}, ${g}, ${b})`;
+ return `rgb(${Math.floor(200 + (255 - 200) * s)}, ${Math.floor(200 + (100 - 200) * s)}, ${Math.floor(220 + (100 - 220) * s)})`;
}
};