graficos de pie chart
This commit is contained in:
BIN
Untitled.png
BIN
Untitled.png
Binary file not shown.
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 223 KiB |
@@ -7,8 +7,8 @@
|
|||||||
<div class="placeholder">Cargando datos…</div>
|
<div class="placeholder">Cargando datos…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Count/percent view in a single card -->
|
<!-- Single unified view with carousel: first slide shows all proportion bars; next slides are pies per group -->
|
||||||
<div v-else-if="viewMode !== 'ratio'" class="card glass">
|
<div v-else class="card glass">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-title">Eventos y comparación</h2>
|
<h2 class="card-title">Eventos y comparación</h2>
|
||||||
<div v-if="filtersCollapsed && (activeFilters?.hasFilters || selectedPlayerUuid)" class="active-filters-summary">
|
<div v-if="filtersCollapsed && (activeFilters?.hasFilters || selectedPlayerUuid)" class="active-filters-summary">
|
||||||
@@ -29,92 +29,98 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bars big">
|
|
||||||
|
<div class="carousel-header">
|
||||||
|
<button class="carousel-btn" @click="prevSlide()" :disabled="!hasPrevSlide">⟨</button>
|
||||||
|
<div class="carousel-title">
|
||||||
|
<span class="title">{{ slideTitle }}</span>
|
||||||
|
<span class="slide-count">{{ currentSlide + 1 }} / {{ totalSlides }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="carousel-btn" @click="nextSlide()" :disabled="!hasNextSlide">⟩</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Slide 0: All ratio bars together -->
|
||||||
|
<div v-if="currentSlide === 0" class="ratio-cards">
|
||||||
<div
|
<div
|
||||||
v-for="eventType in eventTypes"
|
v-for="group in ratioData"
|
||||||
:key="eventType"
|
:key="group.name"
|
||||||
class="bar-row"
|
v-show="group.total > 0"
|
||||||
:class="{ highlight: highlighted === eventType }"
|
class="card glass ratio-card"
|
||||||
@mouseenter="highlighted = eventType"
|
:class="{ highlight: highlighted === group.name }"
|
||||||
|
@mouseenter="highlighted = group.name"
|
||||||
@mouseleave="highlighted = ''"
|
@mouseleave="highlighted = ''"
|
||||||
>
|
>
|
||||||
<div class="bar">
|
<div class="card-header">
|
||||||
<div
|
<h3 class="card-title">{{ group.name }}</h3>
|
||||||
class="bar-fill global shimmer"
|
<span class="group-total">{{ group.total }}</span>
|
||||||
:style="{
|
|
||||||
width: globalBarWidth(eventType) + '%',
|
|
||||||
background: EVENT_STYLES[eventType]?.gradient || 'linear-gradient(90deg, #94a3b8, #64748b)'
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
v-if="selectedPlayerUuid"
|
|
||||||
class="bar-fill player"
|
|
||||||
:style="{
|
|
||||||
width: playerBarWidth(eventType) + '%',
|
|
||||||
background: playerBarGradient
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="bar-chip"
|
|
||||||
:style="{
|
|
||||||
background: getEventChipBg(eventType),
|
|
||||||
borderColor: getEventBorderColor(eventType)
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span class="event-icon">{{ EVENT_STYLES[eventType]?.icon || '📊' }}</span>
|
|
||||||
<span class="chip-label">{{ friendlyEventName(eventType) }}</span>
|
|
||||||
<span class="chip-count global">{{ globalValueLabel(eventType) }}</span>
|
|
||||||
<span v-if="selectedPlayerUuid" class="chip-count player">{{ playerValueLabel(eventType) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="ratio-bar">
|
||||||
<div class="hint small">Basado en mensajes disponibles por sala. Haz clic en un jugador para comparar.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ratio view: one card per group -->
|
|
||||||
<div v-else class="ratio-cards">
|
|
||||||
<div
|
|
||||||
v-for="group in ratioData"
|
|
||||||
:key="group.name"
|
|
||||||
v-show="group.total > 0"
|
|
||||||
class="card glass ratio-card"
|
|
||||||
:class="{ highlight: highlighted === group.name }"
|
|
||||||
@mouseenter="highlighted = group.name"
|
|
||||||
@mouseleave="highlighted = ''"
|
|
||||||
>
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="card-title">{{ group.name }}</h3>
|
|
||||||
<span class="group-total">{{ group.total }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="ratio-bar">
|
|
||||||
<div
|
|
||||||
v-for="(action, actionIndex) in group.actions"
|
|
||||||
:key="action"
|
|
||||||
class="ratio-segment"
|
|
||||||
:style="{
|
|
||||||
width: group.percentages[actionIndex] + '%',
|
|
||||||
background: EVENT_STYLES[action]?.gradient || 'linear-gradient(90deg, #94a3b8, #64748b)'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="ratio-event-chip"
|
v-for="(action, actionIndex) in group.actions"
|
||||||
v-if="group.percentages[actionIndex] > 5"
|
:key="action"
|
||||||
|
class="ratio-segment"
|
||||||
:style="{
|
:style="{
|
||||||
background: getEventChipBg(action),
|
width: group.percentages[actionIndex] + '%',
|
||||||
borderColor: getEventBorderColor(action)
|
background: EVENT_STYLES[action]?.gradient || 'linear-gradient(90deg, #94a3b8, #64748b)'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span class="ratio-icon">{{ EVENT_STYLES[action]?.icon || '📊' }}</span>
|
<div
|
||||||
<span class="ratio-label">{{ group.labels[actionIndex] }}</span>
|
class="ratio-event-chip"
|
||||||
<span class="ratio-count">{{ group.values[actionIndex] }} ({{ Math.round(group.percentages[actionIndex]) }}%)</span>
|
v-if="group.percentages[actionIndex] > 5"
|
||||||
|
:style="{
|
||||||
|
background: getEventChipBg(action),
|
||||||
|
borderColor: getEventBorderColor(action)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="ratio-icon">{{ EVENT_STYLES[action]?.icon || '📊' }}</span>
|
||||||
|
<span class="ratio-label">{{ group.labels[actionIndex] }}</span>
|
||||||
|
<span class="ratio-count">{{ group.values[actionIndex] }} ({{ Math.round(group.percentages[actionIndex]) }}%)</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hint small">
|
||||||
|
{{ selectedPlayerUuid ? 'Proporciones del jugador seleccionado' : 'Proporciones globales' }}.
|
||||||
|
Los segmentos muestran la proporción relativa dentro de cada categoría.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint small">
|
|
||||||
{{ selectedPlayerUuid ? 'Proporciones del jugador seleccionado' : 'Proporciones globales' }}.
|
<!-- Slides 1..N: Pie per group -->
|
||||||
Los segmentos muestran la proporción relativa dentro de cada categoría.
|
<div v-else class="pie-wrapper">
|
||||||
|
<div class="pie-canvas">
|
||||||
|
<svg viewBox="0 0 400 400" preserveAspectRatio="xMidYMid meet" class="pie-svg">
|
||||||
|
<defs>
|
||||||
|
<filter id="pieShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feDropShadow dx="0" dy="6" stdDeviation="6" flood-color="rgba(0,0,0,0.15)" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<circle :cx="center" :cy="center" :r="outerR" class="pie-base" />
|
||||||
|
<g filter="url(#pieShadow)">
|
||||||
|
<path v-for="(seg, i) in currentPieSegments" :key="i"
|
||||||
|
:d="describeArc(center, center, sliceR, seg.startAngle, seg.endAngle)"
|
||||||
|
:fill="EVENT_STYLES[seg.action]?.color || '#94a3b8'"
|
||||||
|
class="pie-slice" />
|
||||||
|
</g>
|
||||||
|
<circle :cx="center" :cy="center" :r="innerR" class="pie-hole" />
|
||||||
|
<g class="pie-center">
|
||||||
|
<text :x="center" :y="center - 4" text-anchor="middle" class="pie-center-title">{{ currentPie?.name || '' }}</text>
|
||||||
|
<text :x="center" :y="center + 16" text-anchor="middle" class="pie-center-sub">{{ selectedPlayerUuid ? 'Jugador' : 'Global' }}</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="pie-legend">
|
||||||
|
<div
|
||||||
|
v-for="(action, idx) in currentPie?.actions || []"
|
||||||
|
:key="action"
|
||||||
|
class="legend-chip"
|
||||||
|
:style="getLegendChipStyle(action)"
|
||||||
|
>
|
||||||
|
<span class="legend-icon">{{ EVENT_STYLES[action]?.icon || '📊' }}</span>
|
||||||
|
<span class="legend-label">{{ currentPie?.labels[idx] }}</span>
|
||||||
|
<span class="legend-count">{{ (currentPie?.values[idx] || 0) }} ({{ round((currentPie?.percentages[idx] || 0)) }}%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hint small">Usa el carrusel para navegar por las categorías.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -131,7 +137,8 @@ interface Props {
|
|||||||
sourceData: string;
|
sourceData: string;
|
||||||
};
|
};
|
||||||
selectedPlayerUuid?: string;
|
selectedPlayerUuid?: string;
|
||||||
viewMode: 'count' | 'percent' | 'ratio';
|
// viewMode no longer used; kept for backward compat
|
||||||
|
viewMode?: 'count' | 'percent' | 'ratio';
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
filtersCollapsed?: boolean;
|
filtersCollapsed?: boolean;
|
||||||
activeFilters?: {
|
activeFilters?: {
|
||||||
@@ -149,6 +156,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
loading: false
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Carousel state: slide 0 = all ratio bars; slides 1..N = pies per group
|
||||||
|
const currentSlide = ref(0);
|
||||||
|
|
||||||
// Event types and styles
|
// Event types and styles
|
||||||
const EVENTS = [
|
const EVENTS = [
|
||||||
'p1_propose', 'p1_no_offer',
|
'p1_propose', 'p1_no_offer',
|
||||||
@@ -238,8 +248,6 @@ const playerEventCounts = computed(() => {
|
|||||||
return playerCounts;
|
return playerCounts;
|
||||||
});
|
});
|
||||||
|
|
||||||
const playerBarGradient = computed(() => '#8b5cf6');
|
|
||||||
|
|
||||||
// Group totals computation
|
// Group totals computation
|
||||||
const groupTotals = computed(() => {
|
const groupTotals = computed(() => {
|
||||||
const counts = globalEventCounts.value;
|
const counts = globalEventCounts.value;
|
||||||
@@ -348,47 +356,51 @@ const ratioData = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global calculations
|
// Carousel + pies
|
||||||
const globalMax = computed(() => {
|
const pieGroups = computed(() => ratioData.value.filter(g => (g.total as number) > 0));
|
||||||
const vals = eventTypes.value.map(k => globalEventCounts.value[k] || 0);
|
const totalSlides = computed(() => 1 + pieGroups.value.length);
|
||||||
const m = Math.max(0, ...vals);
|
const hasPrevSlide = computed(() => currentSlide.value > 0);
|
||||||
return m || 1;
|
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(() =>
|
// Pie geometry based on fixed viewBox (400x400); SVG scales with CSS
|
||||||
eventTypes.value.reduce((acc, k) => acc + (globalEventCounts.value[k] || 0), 0) || 1
|
const center = 200;
|
||||||
);
|
const outerR = 192;
|
||||||
|
const sliceR = 186;
|
||||||
|
const innerR = 102;
|
||||||
|
|
||||||
function globalBarWidth(eventType: string) {
|
function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
|
||||||
const v = globalEventCounts.value[eventType] || 0;
|
const rad = (angleDeg * Math.PI) / 180.0;
|
||||||
return Math.round((v / (props.viewMode === 'percent' ? globalTotal.value : globalMax.value)) * 100);
|
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function globalValueLabel(eventType: string) {
|
function describeArc(cx: number, cy: number, r: number, startAngle: number, endAngle: number) {
|
||||||
const v = globalEventCounts.value[eventType] || 0;
|
const start = polarToCartesian(cx, cy, r, endAngle);
|
||||||
return props.viewMode === 'percent' ? `${Math.round((v / globalTotal.value) * 100)}%` : String(v);
|
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
|
function round(n: number) { return Math.round(n); }
|
||||||
const playerMax = computed(() => {
|
|
||||||
const vals = eventTypes.value.map(k => playerEventCounts.value[k] || 0);
|
|
||||||
const m = Math.max(0, ...vals);
|
|
||||||
return m || 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
const playerTotal = computed(() =>
|
// (Removed legacy bar calculations)
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Styling helpers
|
// Styling helpers
|
||||||
function getEventChipBg(eventType: string): string {
|
function getEventChipBg(eventType: string): string {
|
||||||
@@ -403,21 +415,49 @@ function getEventBorderColor(eventType: string): string {
|
|||||||
return `${style.color}40`;
|
return `${style.color}40`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function friendlyEventName(eventType: string): string {
|
/* (Legacy friendlyEventName removed) */
|
||||||
const friendlyNames: Record<string, string> = {
|
|
||||||
p1_propose: 'Ofrecer',
|
// Legend chip coloring to closely match pie segments
|
||||||
p1_no_offer: 'No Ofrecer',
|
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||||
p2_snatch: 'Robar',
|
const h = hex.replace('#', '').trim();
|
||||||
p2_accept: 'Aceptar Oferta',
|
if (h.length === 3) {
|
||||||
p2_force: 'Forzar Oferta',
|
const r = parseInt(h[0] + h[0], 16);
|
||||||
p2_no_force: 'No Forzar Oferta',
|
const g = parseInt(h[1] + h[1], 16);
|
||||||
p2_reject: 'Rechazar Oferta',
|
const b = parseInt(h[2] + h[2], 16);
|
||||||
p1_shame: 'Asignar Vergüenza',
|
return { r, g, b };
|
||||||
p1_no_shame: 'No Asignar Vergüenza',
|
}
|
||||||
p1_report: 'Denunciar',
|
if (h.length === 6) {
|
||||||
p1_no_report: 'No Denunciar',
|
const r = parseInt(h.slice(0, 2), 16);
|
||||||
};
|
const g = parseInt(h.slice(2, 4), 16);
|
||||||
return friendlyNames[eventType] || eventType;
|
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<string, string>;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -452,6 +492,24 @@ function friendlyEventName(eventType: string): string {
|
|||||||
color: #334155;
|
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 {
|
.ratio-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -606,69 +664,14 @@ function friendlyEventName(eventType: string): string {
|
|||||||
border: 1px solid rgba(99,102,241,0.3);
|
border: 1px solid rgba(99,102,241,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shimmer on global bars for subtle movement */
|
/* (Legacy shimmer removed) */
|
||||||
.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%); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint.small {
|
.hint.small {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ratio bars styles */
|
/* Ratio group 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-total {
|
.group-total {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
@@ -702,6 +705,97 @@ function friendlyEventName(eventType: string): string {
|
|||||||
display: flex;
|
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 {
|
.ratio-segment {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: all 0.6s cubic-bezier(.2,.7,.1,1);
|
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);
|
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) {
|
@media (max-width: 767px) {
|
||||||
.card { padding: 10px 12px; }
|
.card { padding: 10px 12px; }
|
||||||
.card-header { margin-bottom: 8px; }
|
.card-header { margin-bottom: 8px; }
|
||||||
.card-title { font-size: 16px; }
|
.card-title { font-size: 16px; }
|
||||||
.bars.big .bar-row { min-height: 28px; }
|
|
||||||
.ratio-group { min-height: 90px; }
|
.ratio-group { min-height: 90px; }
|
||||||
.ratio-bar { height: 42px; }
|
.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) {
|
@media (max-width: 480px) {
|
||||||
@@ -852,7 +883,6 @@ function friendlyEventName(eventType: string): string {
|
|||||||
.card-title {
|
.card-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.bars.big .bar-row { min-height: 24px; }
|
|
||||||
|
|
||||||
.group-total {
|
.group-total {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user