From a62778a030eafc9945105082fca569710ae567c6 Mon Sep 17 00:00:00 2001 From: egutierrez Date: Thu, 22 May 2025 01:45:57 +0200 Subject: [PATCH] feat: add ECharts and related components for data visualization - Added `echarts` and `echarts-for-react` dependencies to the project. - Created new pages for visualizations: `VisualizacionesRandom` and `Camara_noir`. - Implemented `CanvasDisplay`, `ControlPanel`, `CaptureGrid`, `GridConfigPanel`, and `FrameCard` components for camera functionality. - Integrated WebSocket for real-time image capture in `useCamaraNoir` hook. - Developed FastAPI backend with endpoints for various chart data (bar, line, pie, scatter). - Updated routing to include new analytics paths for visualizations. - Modified submenu links to reflect new analytics options. --- frontend/package-lock.json | 54 ++++- frontend/package.json | 2 + frontend/src/Router.tsx | 10 + .../components/Camara_noir/CanvasDisplay.tsx | 26 +++ .../components/Camara_noir/CaptureGrid.tsx | 74 ++++++ .../components/Camara_noir/ControlPanel.tsx | 25 +++ .../src/components/Camara_noir/FrameCard.tsx | 125 +++++++++++ .../Camara_noir/GridConfigPanel.tsx | 36 +++ frontend/src/components/Camara_noir/index.ts | 6 + .../components/Camara_noir/useCamaraNoir.tsx | 212 ++++++++++++++++++ frontend/src/data/submenuLinks_1.ts | 4 +- frontend/src/pages/Camaras_noir.tsx | 57 +++++ frontend/src/pages/Visualizaciones_Random.tsx | 53 +++++ frontend/yarn.lock | 35 ++- .../emision_datos_fastapi/data_fastapi.py | 69 ++++++ 15 files changed, 784 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/Camara_noir/CanvasDisplay.tsx create mode 100644 frontend/src/components/Camara_noir/CaptureGrid.tsx create mode 100644 frontend/src/components/Camara_noir/ControlPanel.tsx create mode 100644 frontend/src/components/Camara_noir/FrameCard.tsx create mode 100644 frontend/src/components/Camara_noir/GridConfigPanel.tsx create mode 100644 frontend/src/components/Camara_noir/index.ts create mode 100644 frontend/src/components/Camara_noir/useCamaraNoir.tsx create mode 100644 frontend/src/pages/Camaras_noir.tsx create mode 100644 frontend/src/pages/Visualizaciones_Random.tsx create mode 100644 pruebas_conceptos/emision_datos_fastapi/data_fastapi.py diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 36a8836..a263482 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,8 @@ "@tabler/icons": "^3.31.0", "@tabler/icons-react": "^3.31.0", "axios": "^1.9.0", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-rnd": "^10.5.2", @@ -4182,6 +4184,36 @@ "dev": true, "license": "MIT" }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz", + "integrity": "sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4906,7 +4938,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -8596,6 +8627,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/size-sensor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz", + "integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==", + "license": "ISC" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10644,6 +10681,21 @@ "zod": "^3.24.1" } }, + "node_modules/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/zustand": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 93319b7..7296dbd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,8 @@ "@tabler/icons": "^3.31.0", "@tabler/icons-react": "^3.31.0", "axios": "^1.9.0", + "echarts": "^5.6.0", + "echarts-for-react": "^3.0.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-rnd": "^10.5.2", diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 46408a3..272eaed 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -4,6 +4,8 @@ import { Consulta_API } from './pages/Consulta_api'; import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en otra carpeta import { Biblioteca } from './pages/Biblioteca'; +import { VisualizacionesRandom } from './pages/Visualizaciones_Random'; +import { Camara_noir } from './pages/Camaras_noir'; const router = createBrowserRouter([ { @@ -22,6 +24,14 @@ const router = createBrowserRouter([ path: '/Biblioteca', element: , }, + { + path: '/analytics/Visualizaciones_Random', + element: , + }, + { + path: '/analytics/Camaras', + element: , + }, { diff --git a/frontend/src/components/Camara_noir/CanvasDisplay.tsx b/frontend/src/components/Camara_noir/CanvasDisplay.tsx new file mode 100644 index 0000000..d505065 --- /dev/null +++ b/frontend/src/components/Camara_noir/CanvasDisplay.tsx @@ -0,0 +1,26 @@ +// Archivo: components/CanvasDisplay.tsx +import { Box } from '@mantine/core'; +import { RefObject } from 'react'; + +interface CanvasDisplayProps { + canvasRef: RefObject; +} + +export function CanvasDisplay({ canvasRef }: CanvasDisplayProps) { + return ( + + + + ); +} diff --git a/frontend/src/components/Camara_noir/CaptureGrid.tsx b/frontend/src/components/Camara_noir/CaptureGrid.tsx new file mode 100644 index 0000000..2971fbb --- /dev/null +++ b/frontend/src/components/Camara_noir/CaptureGrid.tsx @@ -0,0 +1,74 @@ +// Archivo: components/CaptureGrid.tsx +import { useCallback } from 'react'; +import { SimpleGrid } from '@mantine/core'; +import { FrameCard } from './FrameCard'; + +interface CaptureGridProps { + totalSlots: number; + capturas: string[][]; + setCapturas: (value: string[][]) => void; + frameIndices: number[]; + setFrameIndices: (value: number[]) => void; + fijados: boolean[]; + setFijados: (value: boolean[]) => void; + frameTemporal: number | null; + onScroll: (e: React.WheelEvent, index: number) => void; + onSliderChange: (val: number) => void; + onSliderEnd: (index: number, val: number) => void; + numColumnas: number; +} + +export function CaptureGrid({ + totalSlots, + capturas, + setCapturas, + frameIndices, + setFrameIndices, + fijados, + setFijados, + frameTemporal, + onScroll, + onSliderChange, + onSliderEnd, + numColumnas, +}: CaptureGridProps) { + const handleImageClick = useCallback((index: number, dataUrl: string) => { + const nuevasCapturas = [...capturas]; + nuevasCapturas[index] = [dataUrl]; + setCapturas(nuevasCapturas); + + const nuevosFijados = [...fijados]; + nuevosFijados[index] = true; + setFijados(nuevosFijados); + + const nuevosIndices = [...frameIndices]; + nuevosIndices[index] = 0; + setFrameIndices(nuevosIndices); + }, [capturas, fijados, frameIndices, setCapturas, setFijados, setFrameIndices]); + + return ( + + {Array.from({ length: totalSlots }).map((_, i) => { + const frames = capturas[i] ?? []; + const currentIndex = + !fijados[i] && frameTemporal !== null && frames.length > 0 + ? Math.max(0, Math.min(frames.length - 1, frameTemporal)) + : frameIndices[i]; + + return ( + + ); + })} + + ); +} diff --git a/frontend/src/components/Camara_noir/ControlPanel.tsx b/frontend/src/components/Camara_noir/ControlPanel.tsx new file mode 100644 index 0000000..ef6386c --- /dev/null +++ b/frontend/src/components/Camara_noir/ControlPanel.tsx @@ -0,0 +1,25 @@ +// Archivo: components/ControlPanel.tsx +import { Button, Group } from '@mantine/core'; + +interface ControlPanelProps { + grabando: boolean; + onAlternar: () => void; + onLimpiar: () => void; + onDesfijar: () => void; +} + +export function ControlPanel({ grabando, onAlternar, onLimpiar, onDesfijar }: ControlPanelProps) { + return ( + + + + + + ); +} diff --git a/frontend/src/components/Camara_noir/FrameCard.tsx b/frontend/src/components/Camara_noir/FrameCard.tsx new file mode 100644 index 0000000..e8ce685 --- /dev/null +++ b/frontend/src/components/Camara_noir/FrameCard.tsx @@ -0,0 +1,125 @@ +// Archivo: components/FrameCard.tsx +import { Box, HoverCard, Image, Slider, Stack, Text } from '@mantine/core'; + +interface FrameCardProps { + index: number; + frames: string[]; + currentIndex: number; + fijado: boolean; + onScroll: (e: React.WheelEvent, index: number) => void; + onSliderChange: (val: number) => void; + onSliderEnd: (index: number, val: number) => void; + onImageClick?: (index: number, dataUrl: string) => void; +} + +export function FrameCard({ + index, + frames, + currentIndex, + fijado, + onScroll, + onSliderChange, + onSliderEnd, + onImageClick, +}: FrameCardProps) { + const borderColor = fijado ? '#4caf50' : '#ccc'; + + return ( + + + + {frames.length > 0 && currentIndex < frames.length && ( + <> + {`Captura + + {currentIndex + 1} / {frames.length} + + + )} + + + + onScroll(e, index)}> + {frames.length > 0 && currentIndex < frames.length && ( + + {`Vista { + const imgElement = e.currentTarget; + const img = document.createElement('img'); + img.src = frames[currentIndex]; + await img.decode(); + + const rect = imgElement.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const clickY = e.clientY - rect.top; + const ratioX = clickX / rect.width; + const ratioY = clickY / rect.height; + + const zoomFactor = 2; + const cropWidth = img.width / zoomFactor; + const cropHeight = img.height / zoomFactor; + + const centerX = img.width * ratioX; + const centerY = img.height * ratioY; + + const x = Math.max(0, Math.min(img.width - cropWidth, centerX - cropWidth / 2)); + const y = Math.max(0, Math.min(img.height - cropHeight, centerY - cropHeight / 2)); + + const canvas = document.createElement('canvas'); + canvas.width = cropWidth; + canvas.height = cropHeight; + + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(img, x, y, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight); + const dataUrl = canvas.toDataURL('image/jpeg'); + onImageClick?.(index, dataUrl); + } + }} + /> + onSliderChange(val)} + onChangeEnd={(val) => onSliderEnd(index, val)} + min={0} + max={frames.length - 1} + /> + + )} + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Camara_noir/GridConfigPanel.tsx b/frontend/src/components/Camara_noir/GridConfigPanel.tsx new file mode 100644 index 0000000..82701ef --- /dev/null +++ b/frontend/src/components/Camara_noir/GridConfigPanel.tsx @@ -0,0 +1,36 @@ +// Archivo: components/GridConfigPanel.tsx +import { NumberInput, Stack, Text } from '@mantine/core'; + +interface GridConfigPanelProps { + numFilas: number; + setNumFilas: (val: number) => void; + numColumnas: number; + setNumColumnas: (val: number) => void; +} + +export function GridConfigPanel({ numFilas, setNumFilas, numColumnas, setNumColumnas }: GridConfigPanelProps) { + return ( + + Cartas + setNumFilas(Number(val))} + min={1} + max={5} + step={1} + size="xs" + style={{ width: 60 }} + /> + Jugadores + setNumColumnas(Number(val))} + min={1} + max={11} + step={1} + size="xs" + style={{ width: 60 }} + /> + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Camara_noir/index.ts b/frontend/src/components/Camara_noir/index.ts new file mode 100644 index 0000000..7a6c889 --- /dev/null +++ b/frontend/src/components/Camara_noir/index.ts @@ -0,0 +1,6 @@ +export * from './CanvasDisplay'; +export * from './ControlPanel'; +export * from './GridConfigPanel'; +export * from './CaptureGrid'; +export * from './FrameCard'; +export * from './useCamaraNoir'; \ No newline at end of file diff --git a/frontend/src/components/Camara_noir/useCamaraNoir.tsx b/frontend/src/components/Camara_noir/useCamaraNoir.tsx new file mode 100644 index 0000000..f16af73 --- /dev/null +++ b/frontend/src/components/Camara_noir/useCamaraNoir.tsx @@ -0,0 +1,212 @@ +import { useEffect, useRef, useState } from 'react'; + +export function useCamaraNoir() { + const canvasRef = useRef(null); + const bufferPrevioRef = useRef([]); + const bufferGrabacionRef = useRef([]); + const bufferAcumuladoRef = useRef([]); + const pregrabacionActivaRef = useRef(true); + const primeraGrabacionRealizadaRef = useRef(false); + + const [bufferGlobal, setBufferGlobal] = useState([]); + const [intervaloId, setIntervaloId] = useState | null>(null); + const [grabando, setGrabando] = useState(false); + + const [capturas, setCapturas] = useState([]); + const [frameIndices, setFrameIndices] = useState([]); + const [fijados, setFijados] = useState([]); + const [frameTemporal, setFrameTemporal] = useState(null); + + const [numFilas, setNumFilas] = useState(2); + const [numColumnas, setNumColumnas] = useState(10); + + const DELAY_ENTRE_FRAMES_MS = 10; + const SEGUNDOS_PRE_GRABACION = 5; + const FPS = 1000 / DELAY_ENTRE_FRAMES_MS; + const FRAMES_PRE_GRABACION = Math.floor(FPS * SEGUNDOS_PRE_GRABACION); + const totalSlots = numFilas * numColumnas; + + useEffect(() => { + const id = setInterval(() => { + if (!pregrabacionActivaRef.current) return; + const canvas = canvasRef.current; + if (!canvas) return; + const frame = canvas.toDataURL('image/jpeg'); + bufferPrevioRef.current.push(frame); + if (bufferPrevioRef.current.length > FRAMES_PRE_GRABACION) { + bufferPrevioRef.current.shift(); + } + }, DELAY_ENTRE_FRAMES_MS); + + return () => clearInterval(id); + }, []); + + const desfijarTodos = () => { + setFijados(Array(totalSlots).fill(false)); + }; + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const socket = new WebSocket('ws://10.8.0.9:8000/ws'); + socket.binaryType = 'blob'; + + socket.onmessage = async (event) => { + if (event.data instanceof Blob) { + const imgBitmap = await createImageBitmap(event.data); + ctx.drawImage(imgBitmap, 0, 0, canvas.width, canvas.height); + } + }; + + socket.onerror = (e) => console.error('WebSocket error:', e); + socket.onclose = () => console.warn('WebSocket cerrado'); + + return () => socket.close(); + }, []); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.code === 'Space') { + event.preventDefault(); + alternarGrabacion(); + } else if (event.key.toLowerCase() === 'f') { + event.preventDefault(); + limpiarCapturas(); + } else if (event.key.toLowerCase() === 'd') { + event.preventDefault(); + desfijarTodos(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [grabando, bufferGlobal, totalSlots]); + + useEffect(() => { + const total = numFilas * numColumnas; + const nuevoCapturas = Array(total) + .fill(null) + .map((_, i) => capturas[i] ?? bufferGlobal); + const nuevoIndices = Array(total) + .fill(null) + .map((_, i) => frameIndices[i] ?? (frameTemporal ?? Math.floor(bufferGlobal.length / 2))); + const nuevosFijados = Array(total) + .fill(null) + .map((_, i) => fijados[i] ?? false); + + setCapturas(nuevoCapturas); + setFrameIndices(nuevoIndices); + setFijados(nuevosFijados); + }, [numFilas, numColumnas]); + + const iniciarGrabacion = () => { + if (grabando) return; + setGrabando(true); + bufferGrabacionRef.current = []; + + if (!primeraGrabacionRealizadaRef.current) { + pregrabacionActivaRef.current = false; + } + + const id = setInterval(() => { + const frame = capturarFrame(); + if (frame) { + bufferGrabacionRef.current.push(frame); + } + }, DELAY_ENTRE_FRAMES_MS); + + setIntervaloId(id); + }; + + const capturarFrame = (): string | null => { + const canvas = canvasRef.current; + if (!canvas) return null; + return canvas.toDataURL('image/jpeg'); + }; + + const detenerGrabacion = () => { + if (!grabando) return; + setGrabando(false); + if (intervaloId) clearInterval(intervaloId); + + const nuevaSesion = primeraGrabacionRealizadaRef.current + ? [...bufferGrabacionRef.current] // solo frames nuevos + : [...bufferPrevioRef.current, ...bufferGrabacionRef.current]; + + bufferAcumuladoRef.current.push(...nuevaSesion); + primeraGrabacionRealizadaRef.current = true; + + setBufferGlobal([...bufferAcumuladoRef.current]); + setCapturas(Array(totalSlots).fill([...bufferAcumuladoRef.current])); + const frameInicial = Math.floor(bufferAcumuladoRef.current.length / 2); + setFrameIndices(Array(totalSlots).fill(frameInicial)); + setFijados(Array(totalSlots).fill(false)); + setFrameTemporal(frameInicial); + }; + + const alternarGrabacion = () => { + grabando ? detenerGrabacion() : iniciarGrabacion(); + }; + + const limpiarCapturas = () => { + setCapturas([]); + setFrameIndices([]); + setFijados([]); + setBufferGlobal([]); + bufferGrabacionRef.current = []; + bufferPrevioRef.current = []; + bufferAcumuladoRef.current = []; + primeraGrabacionRealizadaRef.current = false; + pregrabacionActivaRef.current = true; + setFrameTemporal(null); + if (intervaloId) clearInterval(intervaloId); + setGrabando(false); + }; + + const moverFrame = (capturaIndex: number, nuevoIndice: number) => { + setFrameIndices((prev) => { + const nuevos = [...prev]; + const max = capturas[capturaIndex]?.length - 1 ?? 0; + nuevos[capturaIndex] = Math.max(0, Math.min(max, nuevoIndice)); + return nuevos; + }); + }; + + const moverFrameYFijar = (capturaIndex: number, nuevoIndice: number) => { + moverFrame(capturaIndex, nuevoIndice); + setFijados((prev) => { + const actualizados = [...prev]; + actualizados[capturaIndex] = true; + return actualizados; + }); + setFrameTemporal(nuevoIndice); + }; + + const manejarScrollEnSlider = (e: React.WheelEvent, index: number) => { + e.preventDefault(); + if (!fijados[index]) return; + moverFrame(index, frameIndices[index] + (e.deltaY > 0 ? 1 : -1)); + }; + + return { + canvasRef, + bufferGlobal, + grabando, + capturas, + frameIndices, + fijados, + frameTemporal, + numFilas, + setNumFilas, + numColumnas, + setNumColumnas, + alternarGrabacion, + limpiarCapturas, + desfijarTodos, + manejarScrollEnSlider, + moverFrameYFijar, + setFrameTemporal, + }; +} \ No newline at end of file diff --git a/frontend/src/data/submenuLinks_1.ts b/frontend/src/data/submenuLinks_1.ts index 9dd4182..6e31d15 100644 --- a/frontend/src/data/submenuLinks_1.ts +++ b/frontend/src/data/submenuLinks_1.ts @@ -14,8 +14,8 @@ export const submenuLinks = { { label: 'Usuarios', to: '/dashboard/usuarios' }, ], Analytics: [ - { label: 'Conversiones', to: '/analytics/conversiones' }, - { label: 'Tráfico', to: '/analytics/trafico' }, + { label: 'Visualizaciones_Random', to: '/analytics/Visualizaciones_Random' }, + { label: 'Camaras', to: '/analytics/Camaras' }, { label: 'Tendencias', to: '/analytics/tendencias' }, ], Releases: [ diff --git a/frontend/src/pages/Camaras_noir.tsx b/frontend/src/pages/Camaras_noir.tsx new file mode 100644 index 0000000..1ed3bb2 --- /dev/null +++ b/frontend/src/pages/Camaras_noir.tsx @@ -0,0 +1,57 @@ +import { AppShellWithMenu } from '../components/Appshell/Appshell'; +import { Stack, Box } from '@mantine/core'; + +import { + CanvasDisplay, + ControlPanel, + CaptureGrid, + GridConfigPanel, + useCamaraNoir +} from '../components/Camara_noir'; + +export function Camara_noir() { + const camara = useCamaraNoir(); + const totalSlots = camara.numFilas * camara.numColumnas; + + return ( + + + + {/* Contenedor para botones encima del video */} + + + + + + + + {/* Grilla de capturas + configurador lateral */} + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/pages/Visualizaciones_Random.tsx b/frontend/src/pages/Visualizaciones_Random.tsx new file mode 100644 index 0000000..7f6ab8d --- /dev/null +++ b/frontend/src/pages/Visualizaciones_Random.tsx @@ -0,0 +1,53 @@ +import { AppShellWithMenu } from '../components/Appshell/Appshell'; +import { Card, Grid, Title, Loader } from '@mantine/core'; +import { useEffect, useState } from 'react'; +import ReactECharts from 'echarts-for-react'; + +type ChartOption = any; + +function useChartOption(endpoint: string) { + const [option, setOption] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetch(`/api/${endpoint}`) + .then((res) => res.json()) + .then((json) => setOption(json)) + .catch(console.error) + .finally(() => setLoading(false)); + }, [endpoint]); + + return { option, loading }; +} + +export function VisualizacionesRandom() { + const charts = [ + { title: 'Gráfico de barras', endpoint: 'bar' }, + { title: 'Gráfico de líneas', endpoint: 'line' }, + { title: 'Gráfico de pastel', endpoint: 'pie' }, + { title: 'Scatter plot', endpoint: 'scatter' }, + ]; + + return ( + + + {charts.map(({ title, endpoint }, idx) => { + const { option, loading } = useChartOption(endpoint); + + return ( + + + {title} + {loading || !option ? ( + + ) : ( + + )} + + + ); + })} + + + ); +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 88f21c0..1416e05 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1735,6 +1735,22 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +echarts-for-react@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.2.tgz" + integrity sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA== + dependencies: + fast-deep-equal "^3.1.3" + size-sensor "^1.0.1" + +"echarts@^3.0.0 || ^4.0.0 || ^5.0.0", echarts@^5.6.0: + version "5.6.0" + resolved "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz" + integrity sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA== + dependencies: + tslib "2.3.0" + zrender "5.6.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -3810,7 +3826,7 @@ react-use-measure@^2.1.7: resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz" integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg== -"react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react@^18.0.0 || ^19.0.0", "react@^18.x || ^19.x", react@^19.0.0, react@^19.1.0, "react@>= 16", "react@>= 16.3.0", react@>=16.13, react@>=16.3.0, react@>=16.8.0, react@>=17.0, react@>=18, react@>=18.0.0: +"react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^15.0.0 || >=16.0.0", "react@^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react@^18.0.0 || ^19.0.0", "react@^18.x || ^19.x", react@^19.0.0, react@^19.1.0, "react@>= 16", "react@>= 16.3.0", react@>=16.13, react@>=16.3.0, react@>=16.8.0, react@>=17.0, react@>=18, react@>=18.0.0: version "19.1.0" resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== @@ -4145,6 +4161,11 @@ signal-exit@^4.0.1: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +size-sensor@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz" + integrity sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw== + slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" @@ -4620,6 +4641,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +tslib@2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + tslib@2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" @@ -5064,6 +5090,13 @@ zod@^3.23.8, zod@^3.24.1, zod@^3.24.2: resolved "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz" integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg== +zrender@5.6.1: + version "5.6.1" + resolved "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz" + integrity sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag== + dependencies: + tslib "2.3.0" + zustand@^5.0.3: version "5.0.4" resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz" diff --git a/pruebas_conceptos/emision_datos_fastapi/data_fastapi.py b/pruebas_conceptos/emision_datos_fastapi/data_fastapi.py new file mode 100644 index 0000000..01d1515 --- /dev/null +++ b/pruebas_conceptos/emision_datos_fastapi/data_fastapi.py @@ -0,0 +1,69 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +app = FastAPI() + +# Permitir acceso desde el frontend +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # cámbialo por ["http://localhost:5173"] si es necesario + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.get("/api/bar") +def get_bar_chart(): + return { + "xAxis": {"type": "category", "data": ["Ene", "Feb", "Mar", "Abr"]}, + "yAxis": {"type": "value"}, + "series": [{"data": [5, 20, 36, 10], "type": "bar"}] + } + +@app.get("/api/line") +def get_line_chart(): + return { + "xAxis": {"type": "category", "data": ["Semana 1", "Semana 2", "Semana 3", "Semana 4"]}, + "yAxis": {"type": "value"}, + "series": [{"data": [15, 25, 18, 30], "type": "line"}] + } + +@app.get("/api/pie") +def get_pie_chart(): + return { + "tooltip": {"trigger": "item"}, + "legend": {"top": "5%", "left": "center"}, + "series": [{ + "name": "Accesos", + "type": "pie", + "radius": ["40%", "70%"], + "avoidLabelOverlap": False, + "itemStyle": {"borderRadius": 10, "borderColor": "#fff", "borderWidth": 2}, + "label": {"show": False, "position": "center"}, + "emphasis": { + "label": {"show": True, "fontSize": 16, "fontWeight": "bold"} + }, + "labelLine": {"show": False}, + "data": [ + {"value": 1048, "name": "Search"}, + {"value": 735, "name": "Direct"}, + {"value": 580, "name": "Email"}, + {"value": 484, "name": "Union Ads"}, + {"value": 300, "name": "Video Ads"} + ] + }] + } + +@app.get("/api/scatter") +def get_scatter_chart(): + return { + "xAxis": {}, + "yAxis": {}, + "series": [{ + "symbolSize": 20, + "data": [ + [10, 8], [20, 20], [30, 10], [40, 30], [50, 15] + ], + "type": "scatter" + }] + } \ No newline at end of file