import React, { useState, useMemo, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowLeftIcon, LayoutIcon, DatabaseIcon, PlusIcon, TrashIcon, SlidersIcon, TrendUpIcon, FileSvgIcon, } from '@phosphor-icons/react'; import { Link } from 'react-router-dom'; import domtoimage from 'dom-to-image-more'; import Seo from '../../components/Seo'; import CustomDropdown from '../../components/CustomDropdown'; import CustomSlider from '../../components/CustomSlider'; import CustomColorPicker from '../../components/CustomColorPicker'; import CustomToggle from '../../components/CustomToggle'; import BreadcrumbTitle from '../../components/BreadcrumbTitle'; import { useToast } from '../../hooks/useToast'; const CHART_TYPES = [ { label: 'Box Plot (Statistical)', value: 'box' }, { label: 'Frequency Dots', value: 'dot' }, { label: 'Brutalist Bar', value: 'bar' }, { label: 'Metric Signal (Line)', value: 'line' }, { label: 'Pie Chart', value: 'pie' }, ]; const INITIAL_DATA = [ { label: 'Horror', value: 6.2, min: 4.5, q1: 5.8, median: 6.2, q3: 6.8, max: 8.5, color: '#f87171', }, { label: 'Sci-Fi', value: 7.1, min: 5.2, q1: 6.5, median: 7.1, q3: 7.8, max: 9.2, color: '#3b82f6', }, { label: 'Drama', value: 7.8, min: 6.0, q1: 7.2, median: 7.8, q3: 8.4, max: 9.8, color: '#10b981', }, ]; // --- Visualizers --- const BauhausGrid = () => (
); const BoxPlotVis = ({ data, params }) => { const allValues = data.flatMap((d) => [d.min, d.max]); const gMin = Math.min(...allValues) * 0.9; const gMax = Math.max(...allValues) * 1.1; const gRange = gMax - gMin; const mapY = (val) => 360 - ((val - gMin) / (gRange || 1)) * 320; const boxWidth = params.boxWidth || 40; return ( {data.map((item, i) => { const x = 400 + (i - (data.length - 1) / 2) * (600 / (data.length || 1)); const yMin = mapY(item.min); const yQ1 = mapY(item.q1); const yMed = mapY(item.median); const yQ3 = mapY(item.q3); const yMax = mapY(item.max); return ( {params.showValues && ( {item.max} {item.q3} {item.median} {item.q1} {item.min} )} {item.label} {!params.showValues && ( MED: {item.median} Q3: {item.q3} Q1: {item.q1} )} ); })} MAX={gMax.toFixed(1)} MIN={gMin.toFixed(1)} ); }; const DotPlotVis = ({ data, params }) => { const points = useMemo(() => { return data.flatMap((item, idx) => { const count = Math.floor(item.value * params.density); const spacing = 400 / (params.density * 10); const centerX = 400 + (idx - (data.length - 1) / 2) * (600 / (data.length || 1)); return Array.from({ length: count }, (_, i) => ({ id: `${idx}-${i}`, x: centerX + (params.jitter ? (Math.random() - 0.5) * 20 : 0), y: 320 - i * spacing, color: item.color, })); }); }, [data, params.density, params.jitter]); return ( {points.map((p) => ( ))} {data.map((item, i) => { const x = 400 + (i - (data.length - 1) / 2) * (600 / (data.length || 1)); return ( {item.label} ); })} ); }; const BarChartVis = ({ data }) => { const max = Math.max(...data.map((d) => d.value)); return ( {data.map((item, i) => { const barWidth = 500 / (data.length || 1); const xOffset = 400 - 500 / 2; const x = xOffset + i * barWidth; const h = (item.value / (max || 1)) * 250; return ( {item.label} {item.value} ); })} ); }; const LineGraphVis = ({ data, params }) => { const max = Math.max(...data.map((d) => d.value)); const points = data.map((d, i) => ({ x: 400 + (i - (data.length - 1) / 2) * (600 / (data.length - 1 || 1)), y: 320 - (d.value / (max || 1)) * 200, color: d.color, })); const pathD = points.length > 0 ? `M ${points.map((p) => `${p.x},${p.y}`).join(' L ')}` : ''; const areaD = points.length > 0 ? `${pathD} L ${points[points.length - 1].x},320 L ${points[0].x},320 Z` : ''; return ( {params.showArea && points.length > 1 && ( )} {points.length > 1 && ( )} {points.map((p, i) => ( {data[i].label} {data[i].value} ))} ); }; const PieChartVis = ({ data, params }) => { const total = data.reduce((acc, curr) => acc + curr.value, 0); let currentAngle = 0; return ( {data.map((item, i) => { const sliceAngle = (item.value / (total || 1)) * Math.PI * 2; const x1 = Math.cos(currentAngle) * 150; const y1 = Math.sin(currentAngle) * 150; const x2 = Math.cos(currentAngle + sliceAngle) * 150; const y2 = Math.sin(currentAngle + sliceAngle) * 150; const largeArcFlag = sliceAngle > Math.PI ? 1 : 0; const pathData = `M 0 0 L ${x1} ${y1} A 150 150 0 ${largeArcFlag} 1 ${x2} ${y2} Z`; const labelAngle = currentAngle + sliceAngle / 2; const lx = Math.cos(labelAngle) * 180; const ly = Math.sin(labelAngle) * 180; currentAngle += sliceAngle; return ( 0 ? 'start' : 'end'} fontSize="10" style={{ fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', opacity: params.showLabels ? 1 : 0, transition: 'opacity 0.2s', }} className={params.showLabels ? '' : 'group-hover:opacity-100'} > {item.label} ({((item.value / (total || 1)) * 100).toFixed(1)}%) ); })} ); }; // --- Main Component --- const DataPrismPage = () => { const [data, setData] = useState(INITIAL_DATA); const [chartType, setChartType] = useState('box'); const [isPanelOpen, setIsPanelOpen] = useState(true); const visContainerRef = useRef(null); const { addToast } = useToast(); const [visualParams, setVisualParams] = useState({ boxWidth: 40, iqrMultiplier: 1.2, showOutliers: true, density: 8, jitter: false, showArea: true, showLabels: false, showValues: false, metricLineColor: '#ffffff', metricAreaColor: '#ffffff', }); const updateNodeField = (index, field, value) => { const newData = [...data]; newData[index][field] = field === 'label' || field === 'color' ? value : parseFloat(value) || 0; setData(newData); }; const handleAddNode = () => { const colors = [ '#f87171', '#3b82f6', '#10b981', '#fb923c', '#ffffff', '#a78bfa', '#ec4899', ]; setData([ ...data, { label: 'New Node', value: 5.0, min: 2, q1: 4, median: 5, q3: 7, max: 9, color: colors[data.length % colors.length], }, ]); }; const handleRemoveNode = (index) => { if (data.length <= 1) return; setData(data.filter((_, i) => i !== index)); }; const handleExportSVG = () => { if (!visContainerRef.current) return; domtoimage .toSvg(visContainerRef.current, { bgcolor: '#050505', width: 1600, height: 800, style: { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '40px', }, }) .then((dataUrl) => { const link = document.createElement('a'); link.download = `fezcodex-prism-${chartType}.svg`; link.href = dataUrl; link.click(); addToast({ title: 'EXPORT_SVG', message: 'Vector artifact saved.', type: 'success', }); }) .catch((error) => { console.error('SVG Export Failed:', error); addToast({ title: 'EXPORT_ERROR', message: 'SVG generation failed.', type: 'error', }); }); }; return (
{isPanelOpen && (

Visualization_Type

Visual_Params

{chartType === 'box' && (
setVisualParams({ ...visualParams, boxWidth: v }) } min={10} max={100} /> setVisualParams({ ...visualParams, showValues: !visualParams.showValues, }) } />
)} {chartType === 'dot' && ( setVisualParams({ ...visualParams, density: v }) } min={2} max={20} /> )} {chartType === 'line' && (
setVisualParams({ ...visualParams, showArea: !visualParams.showArea, }) } /> setVisualParams({ ...visualParams, metricLineColor: val, }) } /> setVisualParams({ ...visualParams, metricAreaColor: val, }) } />
)} {chartType === 'pie' && ( setVisualParams({ ...visualParams, showLabels: !visualParams.showLabels, }) } /> )}

Dataset_Nodes

{data.map((item, i) => (
updateNodeField(i, 'label', e.target.value) } className="bg-transparent border-none text-[10px] font-mono text-white focus:ring-0 uppercase tracking-widest w-full font-bold" />
{chartType === 'box' ? (
updateNodeField(i, 'min', v)} /> updateNodeField(i, 'max', v)} /> updateNodeField(i, 'q1', v)} /> updateNodeField(i, 'q3', v)} />
updateNodeField(i, 'median', v) } highlight />
) : ( updateNodeField(i, 'value', v)} /> )} updateNodeField(i, 'color', val)} />
))}
)}
{chartType === 'box' && ( )} {chartType === 'dot' && ( )} {chartType === 'bar' && } {chartType === 'line' && ( )} {chartType === 'pie' && ( )}
); }; const MetricInput = ({ label, value, onChange, highlight }) => (
{label} onChange(e.target.value)} className={`w-full bg-black border ${highlight ? 'border-emerald-500/50 text-emerald-400' : 'border-white/10 text-gray-400'} px-2 py-1 font-mono text-[10px] outline-none transition-colors`} />
); export default DataPrismPage;