diff --git a/Untitled.png b/Untitled.png index 9ccb722..ffce2bd 100644 Binary files a/Untitled.png and b/Untitled.png differ diff --git a/client/src/components/EventChart.vue b/client/src/components/EventChart.vue index 567896e..56d4932 100644 --- a/client/src/components/EventChart.vue +++ b/client/src/components/EventChart.vue @@ -7,8 +7,8 @@
Cargando datos…
- -
+ +

Eventos y comparación

@@ -29,92 +29,98 @@
-
+ + + + +
-
-
-
-
- {{ EVENT_STYLES[eventType]?.icon || '📊' }} - {{ friendlyEventName(eventType) }} - {{ globalValueLabel(eventType) }} - {{ playerValueLabel(eventType) }} -
+
+

{{ group.name }}

+ {{ group.total }}
-
-
Basado en mensajes disponibles por sala. Haz clic en un jugador para comparar.
-
-
- - -
-
-
-

{{ group.name }}

- {{ group.total }} -
-
-
+
- {{ EVENT_STYLES[action]?.icon || '📊' }} - {{ group.labels[actionIndex] }} - {{ group.values[actionIndex] }} ({{ Math.round(group.percentages[actionIndex]) }}%) +
+ {{ EVENT_STYLES[action]?.icon || '📊' }} + {{ group.labels[actionIndex] }} + {{ group.values[actionIndex] }} ({{ Math.round(group.percentages[actionIndex]) }}%) +
+
+ {{ selectedPlayerUuid ? 'Proporciones del jugador seleccionado' : 'Proporciones globales' }}. + Los segmentos muestran la proporción relativa dentro de cada categoría. +
-
- {{ selectedPlayerUuid ? 'Proporciones del jugador seleccionado' : 'Proporciones globales' }}. - Los segmentos muestran la proporción relativa dentro de cada categoría. + + +
+
+ + + + + + + + + + + + + {{ currentPie?.name || '' }} + {{ selectedPlayerUuid ? 'Jugador' : 'Global' }} + + +
+
+
+ {{ EVENT_STYLES[action]?.icon || '📊' }} + {{ currentPie?.labels[idx] }} + {{ (currentPie?.values[idx] || 0) }} ({{ round((currentPie?.percentages[idx] || 0)) }}%) +
+
+
Usa el carrusel para navegar por las categorías.
@@ -131,7 +137,8 @@ interface Props { sourceData: string; }; selectedPlayerUuid?: string; - viewMode: 'count' | 'percent' | 'ratio'; + // viewMode no longer used; kept for backward compat + viewMode?: 'count' | 'percent' | 'ratio'; loading?: boolean; filtersCollapsed?: boolean; activeFilters?: { @@ -149,6 +156,9 @@ const props = withDefaults(defineProps(), { loading: false }); +// Carousel state: slide 0 = all ratio bars; slides 1..N = pies per group +const currentSlide = ref(0); + // Event types and styles const EVENTS = [ 'p1_propose', 'p1_no_offer', @@ -238,8 +248,6 @@ const playerEventCounts = computed(() => { return playerCounts; }); -const playerBarGradient = computed(() => '#8b5cf6'); - // Group totals computation const groupTotals = computed(() => { const counts = globalEventCounts.value; @@ -348,47 +356,51 @@ const ratioData = computed(() => { }); }); -// Global calculations -const globalMax = computed(() => { - const vals = eventTypes.value.map(k => globalEventCounts.value[k] || 0); - const m = Math.max(0, ...vals); - return m || 1; +// Carousel + pies +const pieGroups = computed(() => ratioData.value.filter(g => (g.total as number) > 0)); +const totalSlides = computed(() => 1 + pieGroups.value.length); +const hasPrevSlide = computed(() => currentSlide.value > 0); +const hasNextSlide = computed(() => currentSlide.value < totalSlides.value - 1); +function prevSlide() { if (hasPrevSlide.value) currentSlide.value--; } +function nextSlide() { if (hasNextSlide.value) currentSlide.value++; } +const currentPie = computed(() => pieGroups.value[currentSlide.value - 1]); +const slideTitle = computed(() => currentSlide.value === 0 ? 'Proporciones' : (currentPie.value?.name || '')); + +// Convert group percentages into pie segments +const currentPieSegments = computed(() => { + const segments: { startAngle: number; endAngle: number; action: string }[] = []; + if (!currentPie.value) return segments; + let angle = -90; // start at 12 o'clock + (currentPie.value.percentages as number[]).forEach((pct, idx) => { + const span = (pct / 100) * 360; + const seg = { startAngle: angle, endAngle: angle + span, action: currentPie.value!.actions[idx] }; + segments.push(seg); + angle += span; + }); + return segments; }); -const globalTotal = computed(() => - eventTypes.value.reduce((acc, k) => acc + (globalEventCounts.value[k] || 0), 0) || 1 -); +// Pie geometry based on fixed viewBox (400x400); SVG scales with CSS +const center = 200; +const outerR = 192; +const sliceR = 186; +const innerR = 102; -function globalBarWidth(eventType: string) { - const v = globalEventCounts.value[eventType] || 0; - return Math.round((v / (props.viewMode === 'percent' ? globalTotal.value : globalMax.value)) * 100); +function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) { + const rad = (angleDeg * Math.PI) / 180.0; + return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }; } -function globalValueLabel(eventType: string) { - const v = globalEventCounts.value[eventType] || 0; - return props.viewMode === 'percent' ? `${Math.round((v / globalTotal.value) * 100)}%` : String(v); +function describeArc(cx: number, cy: number, r: number, startAngle: number, endAngle: number) { + const start = polarToCartesian(cx, cy, r, endAngle); + const end = polarToCartesian(cx, cy, r, startAngle); + const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1'; + return `M ${cx} ${cy} L ${start.x} ${start.y} A ${r} ${r} 0 ${largeArcFlag} 0 ${end.x} ${end.y} Z`; } -// Player calculations -const playerMax = computed(() => { - const vals = eventTypes.value.map(k => playerEventCounts.value[k] || 0); - const m = Math.max(0, ...vals); - return m || 1; -}); +function round(n: number) { return Math.round(n); } -const playerTotal = computed(() => - eventTypes.value.reduce((acc, k) => acc + (playerEventCounts.value[k] || 0), 0) || 1 -); - -function playerBarWidth(eventType: string) { - const v = playerEventCounts.value[eventType] || 0; - return Math.round((v / (props.viewMode === 'percent' ? playerTotal.value : playerMax.value)) * 100); -} - -function playerValueLabel(eventType: string) { - const v = playerEventCounts.value[eventType] || 0; - return props.viewMode === 'percent' ? `${Math.round((v / playerTotal.value) * 100)}%` : String(v); -} +// (Removed legacy bar calculations) // Styling helpers function getEventChipBg(eventType: string): string { @@ -403,21 +415,49 @@ function getEventBorderColor(eventType: string): string { return `${style.color}40`; } -function friendlyEventName(eventType: string): string { - const friendlyNames: Record = { - p1_propose: 'Ofrecer', - p1_no_offer: 'No Ofrecer', - p2_snatch: 'Robar', - p2_accept: 'Aceptar Oferta', - p2_force: 'Forzar Oferta', - p2_no_force: 'No Forzar Oferta', - p2_reject: 'Rechazar Oferta', - p1_shame: 'Asignar Vergüenza', - p1_no_shame: 'No Asignar Vergüenza', - p1_report: 'Denunciar', - p1_no_report: 'No Denunciar', - }; - return friendlyNames[eventType] || eventType; +/* (Legacy friendlyEventName removed) */ + +// Legend chip coloring to closely match pie segments +function hexToRgb(hex: string): { r: number; g: number; b: number } | null { + const h = hex.replace('#', '').trim(); + if (h.length === 3) { + const r = parseInt(h[0] + h[0], 16); + const g = parseInt(h[1] + h[1], 16); + const b = parseInt(h[2] + h[2], 16); + return { r, g, b }; + } + if (h.length === 6) { + const r = parseInt(h.slice(0, 2), 16); + const g = parseInt(h.slice(2, 4), 16); + const b = parseInt(h.slice(4, 6), 16); + return { r, g, b }; + } + return null; +} + +function luminance(hex: string): number { + const rgb = hexToRgb(hex); + if (!rgb) return 1; // default to light + const srgb = [rgb.r, rgb.g, rgb.b].map(v => { + const c = v / 255; + return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + }); + return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2]; +} + +function rgba(hex: string, alpha: number): string { + const rgb = hexToRgb(hex); + if (!rgb) return `rgba(0,0,0,${alpha})`; + return `rgba(${rgb.r},${rgb.g},${rgb.b},${alpha})`; +} + +function getLegendChipStyle(action: string) { + const style = EVENT_STYLES[action]; + const base = style?.color || '#94a3b8'; + return { + background: base, + borderColor: rgba(base, 0.6) + } as Record; } @@ -452,6 +492,24 @@ function friendlyEventName(eventType: string): string { color: #334155; } +/* Carousel header */ +.carousel-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} +.carousel-title { display: flex; align-items: center; gap: 10px; } +.slide-count { + font-weight: 800; + font-size: 12px; + color: #64748b; + background: rgba(99,102,241,0.06); + border: 1px solid rgba(99,102,241,0.18); + padding: 4px 8px; + border-radius: 999px; +} + .ratio-cards { display: flex; flex-direction: column; @@ -606,69 +664,14 @@ function friendlyEventName(eventType: string): string { border: 1px solid rgba(99,102,241,0.3); } -/* Shimmer on global bars for subtle movement */ -.shimmer::after { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.25) 50%, transparent 70%); - transform: translateX(-100%); - animation: shimmer 3.2s ease-in-out infinite; -} - -@keyframes shimmer { - 0% { transform: translateX(-100%); } - 45% { transform: translateX(110%); } - 100% { transform: translateX(110%); } -} +/* (Legacy shimmer removed) */ .hint.small { font-size: 12px; color: #64748b; } -/* Ratio bars styles */ -.ratio-bars { - display: flex; - flex-direction: column; - gap: 8px; - padding: 8px 0; -} - -.ratio-group { - flex: 0 0 auto; - min-height: 120px; - margin-bottom: 8px; - transition: transform .18s ease; -} - -.ratio-group.highlight { - transform: translateX(4px); -} - -/* Group header styles */ -.ratio-group-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 12px; - margin-bottom: 12px; - padding: 8px 12px; - background: rgba(255, 255, 255, 0.6); - border: 1px solid rgba(229, 231, 235, 0.4); - border-radius: 10px; - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); -} - -.group-title { - margin: 0; - font-size: 18px; - font-weight: 800; - color: #1e293b; - letter-spacing: -0.025em; -} - +/* Ratio group styles */ .group-total { font-size: 16px; font-weight: 900; @@ -702,6 +705,97 @@ function friendlyEventName(eventType: string): string { display: flex; } +/* Pie styles */ +.pie-wrapper { + display: flex; + flex-direction: column; + gap: 10px; +} +.pie-header { + display: flex; + align-items: center; + justify-content: space-between; +} +.pie-title { + display: flex; + align-items: center; + gap: 8px; +} +.carousel-btn { + appearance: none; + border: 1px solid rgba(209, 213, 219, 0.9); + background: white; + color: #334155; + font-weight: 900; + font-size: 16px; + width: 36px; + height: 36px; + border-radius: 10px; + cursor: pointer; +} +.carousel-btn:disabled { opacity: 0.4; cursor: not-allowed; } +.pie-canvas { + display: grid; + place-items: center; + width: min(100%, 70vh); + max-height: 70vh; + aspect-ratio: 1; + margin: 0 auto; +} +.pie-svg { width: 100%; height: 100%; } +.pie-base { + fill: rgba(238,242,255,0.6); + stroke: rgba(199,210,254,0.6); + stroke-width: 1; +} +.pie-slice { stroke: rgba(255,255,255,0.9); stroke-width: 1; } +.pie-hole { + fill: rgba(255,255,255,0.9); + stroke: rgba(229,231,235,0.8); + stroke-width: 1; +} +.pie-center-title { font-weight: 900; font-size: 14px; fill: #1f2937; } +.pie-center-sub { font-weight: 700; font-size: 11px; fill: #64748b; } +.pie-legend { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: clamp(6px, 1.2vw, 12px); +} +.legend-chip { + display: inline-flex; + align-items: center; + gap: clamp(6px, 1.5vw, 18px); + padding: clamp(4px, 1vw, 12px) clamp(8px, 2vw, 24px); + border: 1px solid; + border-radius: clamp(10px, 1.2vw, 14px); + box-shadow: 0 1px 4px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.35); +} +.legend-icon { font-size: clamp(14px, 3vw, 42px); } +.legend-label, .legend-count { + color: #111827; /* black-ish interior */ + font-weight: 900; + /* white outline for legibility */ + text-shadow: + 0 1px 0 #ffffff, + 1px 0 0 #ffffff, + 0 -1px 0 #ffffff, + -1px 0 0 #ffffff, + 1px 1px 0 #ffffff, + -1px 1px 0 #ffffff, + 1px -1px 0 #ffffff, + -1px -1px 0 #ffffff; +} +.legend-label { font-size: clamp(12px, 2.4vw, 36px); } +.legend-count { + padding: clamp(2px, 0.6vw, 6px) clamp(6px, 1.8vw, 18px); + border-radius: 999px; + background: rgba(255,255,255,0.75); + border: 1px solid rgba(229,231,235,0.6); + font-size: clamp(11px, 2.2vw, 33px); +} + .ratio-segment { height: 100%; transition: all 0.6s cubic-bezier(.2,.7,.1,1); @@ -766,75 +860,12 @@ function friendlyEventName(eventType: string): string { box-shadow: 0 1px 3px rgba(0,0,0,0.06); } -/* Responsive chip sizing */ -@media (min-width: 1200px) { - .bar-chip { - min-width: 200px; - padding: 6px 12px; - gap: 8px; - } - - .event-icon { - font-size: 16px; - } - - .chip-label { - font-size: 14px; - } - - .chip-count { - padding: 3px 8px; - font-size: 12px; - } -} - -@media (min-width: 768px) and (max-width: 1199px) { - .bar-chip { - min-width: 140px; - padding: 5px 10px; - gap: 6px; - } - - .event-icon { - font-size: 14px; - } - - .chip-label { - font-size: 12px; - } - - .chip-count { - padding: 2px 6px; - font-size: 10px; - } -} - @media (max-width: 767px) { .card { padding: 10px 12px; } .card-header { margin-bottom: 8px; } .card-title { font-size: 16px; } - .bars.big .bar-row { min-height: 28px; } .ratio-group { min-height: 90px; } .ratio-bar { height: 42px; } - .bar-chip { - min-width: 120px; - padding: 4px 8px; - gap: 4px; - } - - .event-icon { - font-size: 12px; - } - - .chip-label { - font-size: 11px; - } - - .chip-count { - padding: 2px 4px; - font-size: 9px; - margin-left: 2px; - } } @media (max-width: 480px) { @@ -852,7 +883,6 @@ function friendlyEventName(eventType: string): string { .card-title { font-size: 14px; } - .bars.big .bar-row { min-height: 24px; } .group-total { font-size: 12px;