From b7721866e57a84f54f94a73464a633e6d5fc3277 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Thu, 18 Jun 2026 06:48:46 +0800 Subject: [PATCH] fix(stats): implement Model Types chart in Collection tab with correct type distribution --- py/routes/stats_routes.py | 20 +++++++++++- static/js/statistics.js | 65 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/py/routes/stats_routes.py b/py/routes/stats_routes.py index 0ac12348..bd03bd76 100644 --- a/py/routes/stats_routes.py +++ b/py/routes/stats_routes.py @@ -11,6 +11,8 @@ from ..config import config from ..services.settings_manager import get_settings_manager from ..services.server_i18n import server_i18n from ..services.service_registry import ServiceRegistry +from ..services.model_query import normalize_sub_type, resolve_sub_type +from ..utils.constants import VALID_LORA_SUB_TYPES, VALID_CHECKPOINT_SUB_TYPES from ..utils.usage_stats import UsageStats logger = logging.getLogger(__name__) @@ -140,6 +142,21 @@ class StatsRoutes: # Get usage statistics usage_data = await self.usage_stats.get_stats() + # CivitAI model type distribution across all model types + # Use the same logic as the filter panel: normalize_sub_type(resolve_sub_type(entry)) + # with sub-type validation per model type + model_types_counter: Counter[str] = Counter() + for entry in lora_cache.raw_data: + ntype = normalize_sub_type(resolve_sub_type(entry)) + if ntype and ntype in VALID_LORA_SUB_TYPES: + model_types_counter[ntype] += 1 + for entry in checkpoint_cache.raw_data: + ntype = normalize_sub_type(resolve_sub_type(entry)) + if ntype and ntype in VALID_CHECKPOINT_SUB_TYPES: + model_types_counter[ntype] += 1 + # Embeddings: always count as "embedding" regardless of CivitAI sub-type + model_types_counter['embedding'] = len(embedding_cache.raw_data) + return web.json_response({ 'success': True, 'data': { @@ -154,7 +171,8 @@ class StatsRoutes: 'total_generations': usage_data.get('total_executions', 0), 'unused_loras': self._count_unused_models(lora_cache.raw_data, usage_data.get('loras', {})), 'unused_checkpoints': self._count_unused_models(checkpoint_cache.raw_data, usage_data.get('checkpoints', {})), - 'unused_embeddings': self._count_unused_models(embedding_cache.raw_data, usage_data.get('embeddings', {})) + 'unused_embeddings': self._count_unused_models(embedding_cache.raw_data, usage_data.get('embeddings', {})), + 'model_types_distribution': dict(model_types_counter.most_common()) } }) diff --git a/static/js/statistics.js b/static/js/statistics.js index 7c659a96..0cfa2bc0 100644 --- a/static/js/statistics.js +++ b/static/js/statistics.js @@ -240,6 +240,9 @@ export class StatisticsManager { // Storage efficiency chart this.createStorageEfficiencyChart(); + + // Model types chart (Collection tab) + this.createModelTypesChart(); } createCollectionPieChart() { @@ -554,6 +557,68 @@ export class StatisticsManager { }); } + createModelTypesChart() { + const ctx = document.getElementById('modelTypesChart'); + if (!ctx || !this.data.collection || !this.data.collection.model_types_distribution) return; + + const distribution = this.data.collection.model_types_distribution; + const typeDisplayNames = { + lora: 'LoRA', + locon: 'LyCORIS', + dora: 'DoRA', + checkpoint: 'Checkpoint', + diffusion_model: 'Diffusion Model', + embedding: 'Embeddings' + }; + + const colorPalette = { + lora: 'oklch(68% 0.28 256)', + locon: 'oklch(68% 0.25 190)', + dora: 'oklch(68% 0.25 330)', + checkpoint: 'oklch(68% 0.28 45)', + diffusion_model: 'oklch(68% 0.25 280)', + embedding: 'oklch(68% 0.25 120)' + }; + + const labels = Object.keys(distribution).map(k => typeDisplayNames[k] || k); + const values = Object.values(distribution); + const colors = Object.keys(distribution).map(k => colorPalette[k] || 'oklch(68% 0.15 0)'); + + const data = { + labels: labels, + datasets: [{ + data: values, + backgroundColor: colors, + borderColor: getComputedStyle(document.documentElement).getPropertyValue('--border-color'), + borderWidth: 2 + }] + }; + + this.charts.modelTypes = new Chart(ctx, { + type: 'doughnut', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom' + }, + tooltip: { + callbacks: { + label: (context) => { + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const value = context.parsed; + const pct = ((value / total) * 100).toFixed(1); + return ` ${context.label}: ${value} (${pct}%)`; + } + } + } + } + } + }); + } + async initializeLists() { const listTypes = [ { type: 'lora', containerId: 'topLorasList' },