diff --git a/client/src/components/EventChart.vue b/client/src/components/EventChart.vue
index 9c0b481..faa57f9 100644
--- a/client/src/components/EventChart.vue
+++ b/client/src/components/EventChart.vue
@@ -2,7 +2,9 @@
Eventos y comparación
Cargando datos…
-
+
+
+
Basado en mensajes disponibles por sala. Click jugador para comparar.
+
+
+
+
+
+
+
+ {{ eventStyles[action]?.icon || '📊' }}
+ {{ group.labels[actionIndex] }}
+ {{ group.values[actionIndex] }} ({{ Math.round(group.percentages[actionIndex]) }}%)
+
+
+
+
+
+ {{ selectedPlayerUuid ? 'Ratios del jugador seleccionado' : 'Ratios globales' }}.
+ Los segmentos muestran la proporción relativa dentro de cada categoría.
+
+
@@ -56,7 +99,7 @@ interface Props {
playerEventCounts: Record
;
selectedPlayerUuid?: string;
playerBarGradient: string;
- showPercent: boolean;
+ viewMode: 'count' | 'percent' | 'ratio';
loading?: boolean;
}
@@ -67,6 +110,54 @@ const props = withDefaults(defineProps(), {
const highlighted = ref('');
+// Define ratio groups for superposed view
+const ratioGroups = [
+ {
+ name: 'Ofertas',
+ actions: ['p1_propose', 'p1_no_offer'],
+ labels: ['Ofrecer', 'No Ofrecer']
+ },
+ {
+ name: 'Respuestas',
+ actions: ['p2_accept', 'p2_reject', 'p2_snatch'],
+ labels: ['Aceptar', 'Rechazar', 'Robar']
+ },
+ {
+ name: 'Fuerzas',
+ actions: ['p2_force', 'p2_no_force'],
+ labels: ['Forzar', 'No Forzar']
+ },
+ {
+ name: 'Vergüenzas',
+ actions: ['p1_shame', 'p1_no_shame'],
+ labels: ['Asignar', 'No Asignar']
+ },
+ {
+ name: 'Denuncias',
+ actions: ['p1_report', 'p1_no_report'],
+ labels: ['Denunciar', 'No Denunciar']
+ }
+];
+
+// Compute ratio data for each group
+const ratioData = computed(() => {
+ return ratioGroups.map(group => {
+ const counts = props.selectedPlayerUuid
+ ? props.playerEventCounts
+ : props.globalEventCounts;
+
+ const values = group.actions.map(action => counts[action] || 0);
+ const total = values.reduce((sum, val) => sum + val, 0);
+
+ return {
+ ...group,
+ values,
+ total,
+ percentages: total > 0 ? values.map(val => (val / total) * 100) : values.map(() => 0)
+ };
+ });
+});
+
// Global calculations
const globalMax = computed(() => {
const vals = props.eventTypes.map(k => props.globalEventCounts[k] || 0);
@@ -80,12 +171,12 @@ const globalTotal = computed(() =>
function globalBarWidth(eventType: string) {
const v = props.globalEventCounts[eventType] || 0;
- return Math.round((v / (props.showPercent ? globalTotal.value : globalMax.value)) * 100);
+ return Math.round((v / (props.viewMode === 'percent' ? globalTotal.value : globalMax.value)) * 100);
}
function globalValueLabel(eventType: string) {
const v = props.globalEventCounts[eventType] || 0;
- return props.showPercent ? `${Math.round((v / globalTotal.value) * 100)}%` : String(v);
+ return props.viewMode === 'percent' ? `${Math.round((v / globalTotal.value) * 100)}%` : String(v);
}
// Player calculations
@@ -101,12 +192,12 @@ const playerTotal = computed(() =>
function playerBarWidth(eventType: string) {
const v = props.playerEventCounts[eventType] || 0;
- return Math.round((v / (props.showPercent ? playerTotal.value : playerMax.value)) * 100);
+ return Math.round((v / (props.viewMode === 'percent' ? playerTotal.value : playerMax.value)) * 100);
}
function playerValueLabel(eventType: string) {
const v = props.playerEventCounts[eventType] || 0;
- return props.showPercent ? `${Math.round((v / playerTotal.value) * 100)}%` : String(v);
+ return props.viewMode === 'percent' ? `${Math.round((v / playerTotal.value) * 100)}%` : String(v);
}
// Styling helpers
@@ -310,6 +401,101 @@ function friendlyEventName(eventType: string): string {
color: #64748b;
}
+/* Ratio bars styles */
+.ratio-bars {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ flex: 1 1 auto;
+ min-height: 0;
+ padding: 8px 0;
+}
+
+.ratio-group {
+ flex: 1 1 0;
+ min-height: 60px;
+ transition: transform .18s ease;
+}
+
+.ratio-group.highlight {
+ transform: translateX(4px);
+}
+
+.ratio-bar {
+ position: relative;
+ height: 100%;
+ background: linear-gradient(135deg, rgba(238,242,255,0.4) 0%, rgba(199,210,254,0.2) 100%);
+ border-radius: 12px;
+ overflow: hidden;
+ border: 1px solid rgba(199,210,254,0.3);
+ width: 100%;
+ display: flex;
+}
+
+.ratio-segment {
+ height: 100%;
+ transition: all 0.6s cubic-bezier(.2,.7,.1,1);
+ backdrop-filter: blur(4px);
+ opacity: 0.8;
+}
+
+.ratio-segment:first-child {
+ border-top-left-radius: 12px;
+ border-bottom-left-radius: 12px;
+}
+
+.ratio-segment:last-child {
+ border-top-right-radius: 12px;
+ border-bottom-right-radius: 12px;
+}
+
+.ratio-event-chip {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 10px;
+ border-radius: 999px;
+ border: 1px solid;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.4);
+ backdrop-filter: blur(8px) saturate(120%);
+ -webkit-backdrop-filter: blur(8px) saturate(120%);
+ transition: all 0.3s ease;
+ white-space: nowrap;
+ max-width: 95%;
+}
+
+.ratio-segment:hover .ratio-event-chip {
+ transform: translate(-50%, -50%) scale(1.05);
+ box-shadow: 0 6px 16px rgba(0,0,0,0.12), inset 0 1px 0 rgba(255,255,255,0.6);
+}
+
+.ratio-icon {
+ font-size: 14px;
+}
+
+.ratio-label {
+ color: #0f172a;
+ font-weight: 800;
+ font-size: 13px;
+ letter-spacing: .1px;
+}
+
+.ratio-count {
+ color: #1f2937;
+ font-weight: 800;
+ background: rgba(255,255,255,0.7);
+ border: 1px solid rgba(229,231,235,0.5);
+ border-radius: 999px;
+ padding: 2px 6px;
+ font-size: 11px;
+ margin-left: 2px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
+}
+
/* Responsive chip sizing */
@media (min-width: 1200px) {
.bar-chip {
@@ -395,5 +581,83 @@ function friendlyEventName(eventType: string): string {
font-size: 8px;
margin-left: 1px;
}
+
+ /* Ratio responsive styles */
+ .ratio-event-chip {
+ padding: 3px 6px;
+ gap: 3px;
+ }
+
+ .ratio-icon {
+ font-size: 11px;
+ }
+
+ .ratio-label {
+ font-size: 10px;
+ }
+
+ .ratio-count {
+ font-size: 8px;
+ padding: 1px 3px;
+ }
+}
+
+@media (max-width: 767px) {
+ .ratio-event-chip {
+ padding: 3px 7px;
+ gap: 4px;
+ }
+
+ .ratio-icon {
+ font-size: 12px;
+ }
+
+ .ratio-label {
+ font-size: 11px;
+ }
+
+ .ratio-count {
+ font-size: 9px;
+ padding: 1px 4px;
+ }
+}
+
+@media (min-width: 768px) and (max-width: 1199px) {
+ .ratio-event-chip {
+ padding: 4px 8px;
+ gap: 5px;
+ }
+
+ .ratio-icon {
+ font-size: 13px;
+ }
+
+ .ratio-label {
+ font-size: 12px;
+ }
+
+ .ratio-count {
+ font-size: 10px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .ratio-event-chip {
+ padding: 6px 12px;
+ gap: 8px;
+ }
+
+ .ratio-icon {
+ font-size: 16px;
+ }
+
+ .ratio-label {
+ font-size: 14px;
+ }
+
+ .ratio-count {
+ font-size: 12px;
+ padding: 3px 8px;
+ }
}
\ No newline at end of file
diff --git a/client/src/views/Leaderboard.vue b/client/src/views/Leaderboard.vue
index 1e81888..0697826 100644
--- a/client/src/views/Leaderboard.vue
+++ b/client/src/views/Leaderboard.vue
@@ -4,8 +4,8 @@
📈 Leaderboard
{{ loading ? 'Actualizando…' : 'Actualizar' }}
-
- {{ showPercent ? 'Ver conteos' : 'Ver %' }}
+
+ {{ viewModeLabel }}
@@ -29,7 +29,14 @@
Jugador
@@ -60,7 +62,7 @@
:player-event-counts="playerEventCounts"
:selected-player-uuid="selectedUuid"
:player-bar-gradient="playerBarGradient"
- :show-percent="showPercent"
+ :view-mode="viewMode"
:loading="loading"
/>
@@ -78,6 +80,27 @@ interface RoomState { players?: any[]; systemMessages?: { kind: string }[] }
const loading = ref(false);
const eventFilters = useEventFilters();
+
+// View mode cycling
+type ViewMode = 'count' | 'percent' | 'ratio';
+const viewMode = ref('count');
+
+const viewModeLabel = computed(() => {
+ switch (viewMode.value) {
+ case 'count': return 'Ver conteos';
+ case 'percent': return 'Ver %';
+ case 'ratio': return 'Ver superpuesto';
+ default: return 'Ver conteos';
+ }
+});
+
+function cycleViewMode() {
+ const modes: ViewMode[] = ['count', 'percent', 'ratio'];
+ const currentIndex = modes.indexOf(viewMode.value);
+ const nextIndex = (currentIndex + 1) % modes.length;
+ viewMode.value = modes[nextIndex];
+}
+
const EVENTS = [
'p1_propose', 'p1_no_offer',
'p2_snatch', 'p2_accept', 'p2_force', 'p2_no_force', 'p2_reject',
@@ -98,7 +121,6 @@ const EVENT_STYLES: Record {
@@ -119,11 +141,25 @@ watch(search, () => {
page.value = 1;
});
const page = ref(1);
-const pageSize = 20;
-const pageCount = computed(() => Math.max(1, Math.ceil((playersFiltered.value.length || 0) / pageSize)));
+const containerWidth = ref(1200); // Default width, will be updated dynamically
+const dynamicPageSize = computed(() => {
+ // Estimate space needed per chip and controls
+ const chipWidth = 140; // Average chip width including gap
+ const clearBtnWidth = selectedUuid.value ? 150 : 0; // "Quitar selección" button
+ const searchWidth = 240; // Search input
+ const paginationWidth = pageCount.value > 1 ? 100 : 0; // Compact pagination
+ const margin = 40; // Container margins
+
+ const availableWidth = containerWidth.value - searchWidth - paginationWidth - clearBtnWidth - margin;
+ const maxChips = Math.max(3, Math.floor(availableWidth / chipWidth)); // Minimum 3 chips
+
+ return Math.min(maxChips, 15); // Maximum 15 chips per page
+});
+
+const pageCount = computed(() => Math.max(1, Math.ceil((playersFiltered.value.length || 0) / dynamicPageSize.value)));
const playersPage = computed(() => {
- const start = (page.value - 1) * pageSize;
- return playersFiltered.value.slice(start, start + pageSize);
+ const start = (page.value - 1) * dynamicPageSize.value;
+ return playersFiltered.value.slice(start, start + dynamicPageSize.value);
});
const selectedUuid = ref('');
@@ -368,12 +404,25 @@ async function loadPlayerHistory() {
playerLoading.value = false;
}
+// Update container width on resize
+function updateContainerWidth() {
+ containerWidth.value = window.innerWidth;
+}
+
onMounted(() => {
setupStreams();
// Initialize with aggregated data as default
eventFilters.globalEventCounts.value = { ...eventFilters.globalEventCountsAggregated.value };
+
+ // Set initial container width and add resize listener
+ updateContainerWidth();
+ window.addEventListener('resize', updateContainerWidth);
+});
+
+onUnmounted(() => {
+ closeStreams();
+ window.removeEventListener('resize', updateContainerWidth);
});
-onUnmounted(closeStreams);
// Reset to first page when search changes or players list length shrinks below current page
watch(() => search.value, () => { page.value = 1; });
@@ -406,10 +455,41 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([
.key.player { background: linear-gradient(90deg, #a78bfa, #6366f1); box-shadow: 0 0 8px rgba(99,102,241,0.35); }
.sep { opacity: 0.6; }
-.player-chips { display:flex; align-items:flex-start; gap: 10px; flex-wrap: wrap; }
-.search { padding:8px 10px; border:1px solid #cbd5e1; background:#fff; color:#0f172a; border-radius: 10px; min-width: 240px; outline:none; }
-.search::placeholder { color:#64748b; }
-.chips { display:flex; gap:10px; flex-wrap: wrap; }
+.player-chips {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.search-controls {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ justify-content: space-between;
+}
+
+.search {
+ padding: 8px 10px;
+ border: 1px solid #cbd5e1;
+ background: #fff;
+ color: #0f172a;
+ border-radius: 10px;
+ min-width: 240px;
+ outline: none;
+ flex: 1;
+ max-width: 300px;
+}
+
+.search::placeholder {
+ color: #64748b;
+}
+
+.chips {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ align-items: center;
+}
.chip { display:flex; align-items:center; gap:8px; background: color-mix(in srgb, var(--primary) 6%, white); border:1px solid color-mix(in srgb, var(--primary) 24%, #e5e7eb); padding:8px 12px; border-radius: 999px; color:#111827; cursor:pointer; transition: transform .18s ease, background .18s ease, box-shadow .18s ease; }
.chip:hover { transform: translateY(-1px); background: color-mix(in srgb, var(--primary) 10%, white); box-shadow: 0 6px 18px rgba(102,126,234,0.18); }
.chip.active { background: color-mix(in srgb, var(--primary) 18%, white); border-color: color-mix(in srgb, var(--primary) 45%, #c7d2fe); box-shadow: 0 6px 22px rgba(99,102,241,0.22); }
@@ -422,16 +502,21 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([
.pagination {
display: flex;
align-items: center;
- gap: 12px;
- padding: 8px 12px;
+ gap: 8px;
+ padding: 6px 10px;
background: rgba(255,255,255,0.82);
border: 1px solid rgba(229,231,235,0.9);
border-radius: 999px;
- box-shadow: 0 6px 18px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.6);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.6);
backdrop-filter: blur(10px) saturate(120%);
-webkit-backdrop-filter: blur(10px) saturate(120%);
}
+.pagination.compact {
+ gap: 6px;
+ padding: 4px 8px;
+}
+
.pg-btn {
width: 32px;
height: 32px;
@@ -449,11 +534,23 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([
box-shadow: 0 4px 12px rgba(102,126,234,0.25);
}
+.pg-btn.compact {
+ width: 24px;
+ height: 24px;
+ font-size: 14px;
+ font-weight: 800;
+ box-shadow: 0 2px 8px rgba(102,126,234,0.2);
+}
+
.pg-btn:hover:not(:disabled) {
- transform: translateY(-2px);
+ transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(102,126,234,0.35);
}
+.pg-btn.compact:hover:not(:disabled) {
+ box-shadow: 0 4px 12px rgba(102,126,234,0.3);
+}
+
.pg-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
@@ -463,23 +560,54 @@ const allPlayersActions = ref<{ uuid: string; name: string; total: number }[]>([
}
.pg-ind {
- font-weight: 700;
+ font-weight: 600;
color: #334155;
- font-size: 14px;
- min-width: 60px;
+ font-size: 12px;
+ min-width: 35px;
text-align: center;
}
-@media (max-width: 480px) {
- .player-chips {
+@media (max-width: 768px) {
+ .search-controls {
flex-direction: column;
+ align-items: stretch;
gap: 8px;
}
- .pagination {
- width: 100%;
+ .search {
+ min-width: auto;
+ max-width: none;
+ }
+
+ .pagination.compact {
+ align-self: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .player-chips {
+ gap: 8px;
+ }
+
+ .chips {
+ gap: 8px;
justify-content: center;
}
+
+ .chip {
+ padding: 6px 10px;
+ }
+
+ .pg-btn.compact {
+ width: 20px;
+ height: 20px;
+ font-size: 12px;
+ }
+
+ .pg-ind {
+ font-size: 11px;
+ min-width: 30px;
+ }
}