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.
This commit is contained in:
Generated
+53
-1
@@ -14,6 +14,8 @@
|
|||||||
"@tabler/icons": "^3.31.0",
|
"@tabler/icons": "^3.31.0",
|
||||||
"@tabler/icons-react": "^3.31.0",
|
"@tabler/icons-react": "^3.31.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"echarts-for-react": "^3.0.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-rnd": "^10.5.2",
|
"react-rnd": "^10.5.2",
|
||||||
@@ -4182,6 +4184,36 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -4906,7 +4938,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
@@ -8596,6 +8627,12 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/slash": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||||
@@ -10644,6 +10681,21 @@
|
|||||||
"zod": "^3.24.1"
|
"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": {
|
"node_modules/zustand": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
"@tabler/icons": "^3.31.0",
|
"@tabler/icons": "^3.31.0",
|
||||||
"@tabler/icons-react": "^3.31.0",
|
"@tabler/icons-react": "^3.31.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"echarts-for-react": "^3.0.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-rnd": "^10.5.2",
|
"react-rnd": "^10.5.2",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Consulta_API } from './pages/Consulta_api';
|
|||||||
import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta
|
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 { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en otra carpeta
|
||||||
import { Biblioteca } from './pages/Biblioteca';
|
import { Biblioteca } from './pages/Biblioteca';
|
||||||
|
import { VisualizacionesRandom } from './pages/Visualizaciones_Random';
|
||||||
|
import { Camara_noir } from './pages/Camaras_noir';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -22,6 +24,14 @@ const router = createBrowserRouter([
|
|||||||
path: '/Biblioteca',
|
path: '/Biblioteca',
|
||||||
element: <Biblioteca />,
|
element: <Biblioteca />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/analytics/Visualizaciones_Random',
|
||||||
|
element: <VisualizacionesRandom />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/analytics/Camaras',
|
||||||
|
element: <Camara_noir />,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Archivo: components/CanvasDisplay.tsx
|
||||||
|
import { Box } from '@mantine/core';
|
||||||
|
import { RefObject } from 'react';
|
||||||
|
|
||||||
|
interface CanvasDisplayProps {
|
||||||
|
canvasRef: RefObject<HTMLCanvasElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanvasDisplay({ canvasRef }: CanvasDisplayProps) {
|
||||||
|
return (
|
||||||
|
<Box style={{ position: 'relative', width: '100%', height: 480 }}>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={640}
|
||||||
|
height={480}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'contain',
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<SimpleGrid cols={numColumnas} style={{ flex: 1, columnGap: 12, rowGap: 4 }}>
|
||||||
|
{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 (
|
||||||
|
<FrameCard
|
||||||
|
key={i}
|
||||||
|
index={i}
|
||||||
|
frames={frames}
|
||||||
|
currentIndex={currentIndex}
|
||||||
|
fijado={fijados[i]}
|
||||||
|
onScroll={onScroll}
|
||||||
|
onSliderChange={onSliderChange}
|
||||||
|
onSliderEnd={onSliderEnd}
|
||||||
|
onImageClick={handleImageClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SimpleGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Group gap="sm">
|
||||||
|
<Button onClick={onAlternar} variant="light" color={grabando ? 'orange' : 'blue'}>
|
||||||
|
{grabando ? 'Grabando... (Presiona Espacio)' : 'Iniciar grabación'}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onLimpiar} variant="filled" color="red">
|
||||||
|
Eliminar imágenes
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onDesfijar} variant="default" color="gray">
|
||||||
|
Desfijar todos (D)
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<HoverCard key={index} width={700} shadow="md" position="top" withArrow>
|
||||||
|
<HoverCard.Target>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 160,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 8,
|
||||||
|
border: `2px solid ${borderColor}`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
background: '#f1f1f1',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{frames.length > 0 && currentIndex < frames.length && (
|
||||||
|
<>
|
||||||
|
<Image
|
||||||
|
src={frames[currentIndex]}
|
||||||
|
alt={`Captura ${index}`}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 4,
|
||||||
|
right: 6,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: '2px 4px',
|
||||||
|
fontSize: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentIndex + 1} / {frames.length}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</HoverCard.Target>
|
||||||
|
|
||||||
|
<HoverCard.Dropdown p="xs" onWheel={(e) => onScroll(e, index)}>
|
||||||
|
{frames.length > 0 && currentIndex < frames.length && (
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Image
|
||||||
|
src={frames[currentIndex]}
|
||||||
|
alt={`Vista ampliada ${index}`}
|
||||||
|
style={{ width: '100%', height: 'auto', maxWidth: '100%', cursor: 'zoom-in' }}
|
||||||
|
fit="contain"
|
||||||
|
onClick={async (e) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
value={currentIndex}
|
||||||
|
onChange={(val) => onSliderChange(val)}
|
||||||
|
onChangeEnd={(val) => onSliderEnd(index, val)}
|
||||||
|
min={0}
|
||||||
|
max={frames.length - 1}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</HoverCard.Dropdown>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<Stack align="center" gap="xs" ml="md">
|
||||||
|
<Text size="sm">Cartas</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={numFilas}
|
||||||
|
onChange={(val) => setNumFilas(Number(val))}
|
||||||
|
min={1}
|
||||||
|
max={5}
|
||||||
|
step={1}
|
||||||
|
size="xs"
|
||||||
|
style={{ width: 60 }}
|
||||||
|
/>
|
||||||
|
<Text size="sm">Jugadores</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={numColumnas}
|
||||||
|
onChange={(val) => setNumColumnas(Number(val))}
|
||||||
|
min={1}
|
||||||
|
max={11}
|
||||||
|
step={1}
|
||||||
|
size="xs"
|
||||||
|
style={{ width: 60 }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './CanvasDisplay';
|
||||||
|
export * from './ControlPanel';
|
||||||
|
export * from './GridConfigPanel';
|
||||||
|
export * from './CaptureGrid';
|
||||||
|
export * from './FrameCard';
|
||||||
|
export * from './useCamaraNoir';
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
export function useCamaraNoir() {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
const bufferPrevioRef = useRef<string[]>([]);
|
||||||
|
const bufferGrabacionRef = useRef<string[]>([]);
|
||||||
|
const bufferAcumuladoRef = useRef<string[]>([]);
|
||||||
|
const pregrabacionActivaRef = useRef(true);
|
||||||
|
const primeraGrabacionRealizadaRef = useRef(false);
|
||||||
|
|
||||||
|
const [bufferGlobal, setBufferGlobal] = useState<string[]>([]);
|
||||||
|
const [intervaloId, setIntervaloId] = useState<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const [grabando, setGrabando] = useState(false);
|
||||||
|
|
||||||
|
const [capturas, setCapturas] = useState<string[][]>([]);
|
||||||
|
const [frameIndices, setFrameIndices] = useState<number[]>([]);
|
||||||
|
const [fijados, setFijados] = useState<boolean[]>([]);
|
||||||
|
const [frameTemporal, setFrameTemporal] = useState<number | null>(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@ export const submenuLinks = {
|
|||||||
{ label: 'Usuarios', to: '/dashboard/usuarios' },
|
{ label: 'Usuarios', to: '/dashboard/usuarios' },
|
||||||
],
|
],
|
||||||
Analytics: [
|
Analytics: [
|
||||||
{ label: 'Conversiones', to: '/analytics/conversiones' },
|
{ label: 'Visualizaciones_Random', to: '/analytics/Visualizaciones_Random' },
|
||||||
{ label: 'Tráfico', to: '/analytics/trafico' },
|
{ label: 'Camaras', to: '/analytics/Camaras' },
|
||||||
{ label: 'Tendencias', to: '/analytics/tendencias' },
|
{ label: 'Tendencias', to: '/analytics/tendencias' },
|
||||||
],
|
],
|
||||||
Releases: [
|
Releases: [
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<AppShellWithMenu>
|
||||||
|
<Stack p="md" gap="md">
|
||||||
|
|
||||||
|
{/* Contenedor para botones encima del video */}
|
||||||
|
<Box style={{ position: 'relative', width: '100%' }}>
|
||||||
|
<ControlPanel
|
||||||
|
grabando={camara.grabando}
|
||||||
|
onAlternar={camara.alternarGrabacion}
|
||||||
|
onLimpiar={camara.limpiarCapturas}
|
||||||
|
onDesfijar={camara.desfijarTodos}
|
||||||
|
/>
|
||||||
|
<Box style={{ marginTop: 8 }}>
|
||||||
|
<CanvasDisplay canvasRef={camara.canvasRef} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Grilla de capturas + configurador lateral */}
|
||||||
|
<Box style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||||||
|
<CaptureGrid
|
||||||
|
totalSlots={totalSlots}
|
||||||
|
capturas={camara.capturas}
|
||||||
|
frameIndices={camara.frameIndices}
|
||||||
|
fijados={camara.fijados}
|
||||||
|
frameTemporal={camara.frameTemporal}
|
||||||
|
onScroll={camara.manejarScrollEnSlider}
|
||||||
|
onSliderChange={camara.setFrameTemporal}
|
||||||
|
onSliderEnd={camara.moverFrameYFijar}
|
||||||
|
numColumnas={camara.numColumnas}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GridConfigPanel
|
||||||
|
numFilas={camara.numFilas}
|
||||||
|
setNumFilas={camara.setNumFilas}
|
||||||
|
numColumnas={camara.numColumnas}
|
||||||
|
setNumColumnas={camara.setNumColumnas}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</AppShellWithMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<ChartOption | null>(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 (
|
||||||
|
<AppShellWithMenu>
|
||||||
|
<Grid>
|
||||||
|
{charts.map(({ title, endpoint }, idx) => {
|
||||||
|
const { option, loading } = useChartOption(endpoint);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid.Col span={{ base: 12, sm: 6, md: 6, lg: 3 }} key={idx}>
|
||||||
|
<Card shadow="sm" padding="lg" radius="md" withBorder>
|
||||||
|
<Title order={4}>{title}</Title>
|
||||||
|
{loading || !option ? (
|
||||||
|
<Loader mt="md" />
|
||||||
|
) : (
|
||||||
|
<ReactECharts option={option} style={{ height: 250, marginTop: 16 }} />
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
</AppShellWithMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
+34
-1
@@ -1735,6 +1735,22 @@ eastasianwidth@^0.2.0:
|
|||||||
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
|
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
|
||||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
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:
|
ee-first@1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
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"
|
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
|
||||||
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
|
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"
|
version "19.1.0"
|
||||||
resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz"
|
resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz"
|
||||||
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
|
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"
|
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz"
|
||||||
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
|
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:
|
slash@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
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:
|
tslib@2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
|
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"
|
resolved "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz"
|
||||||
integrity sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==
|
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:
|
zustand@^5.0.3:
|
||||||
version "5.0.4"
|
version "5.0.4"
|
||||||
resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz"
|
resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user