Closed jiwnchoi closed 55 minutes ago
클로드가 짜준 예시 코드
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Undo } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { LineChart, BarChart, ScatterChart, Line, Bar, Scatter, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import _ from 'lodash';
import Papa from 'papaparse';
// 시각화 타입 정의
const CHART_TYPES = ['bar', 'line', 'scatter'];
const DataExplorer = () => {
const [data, setData] = useState([]);
const [attributes, setAttributes] = useState([]);
const [selectedAttributes, setSelectedAttributes] = useState([]);
const [chartType, setChartType] = useState('bar');
const [chartOrientation, setChartOrientation] = useState('vertical');
const [file, setFile] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [touchStart, setTouchStart] = useState(null);
const [touchEnd, setTouchEnd] = useState(null);
const chartRef = useRef(null);
// Helper functions for MMI calculation
const calculateEntropy = (values) => {
const total = values.length;
const counts = _.countBy(values);
return -Object.values(counts).reduce((sum, count) => {
const probability = count / total;
return sum + probability * Math.log2(probability);
}, 0);
};
const calculateJointEntropy = (attributes) => {
const jointDistribution = data.map(row =>
attributes.map(attr => row[attr]).join('|')
);
return calculateEntropy(jointDistribution);
};
const calculateMMI = (attributes) => {
if (attributes.length === 0) return 0;
let mmi = 0;
for (let k = 1; k <= attributes.length; k++) {
const combinations = _.combinations(attributes, k);
const term = combinations.reduce((sum, combination) => {
return sum + calculateJointEntropy(combination);
}, 0);
mmi += Math.pow(-1, k - 1) * term;
}
return mmi;
};
const getDataType = (attribute) => {
const values = data.map(row => row[attribute]);
const uniqueValues = new Set(values);
if (values.every(v => typeof v === 'number')) {
return 'quantitative';
}
if (uniqueValues.size <= values.length * 0.05) {
return 'nominal';
}
if (values.every(v => !isNaN(Date.parse(v)))) {
return 'temporal';
}
return 'nominal';
};
// Memoize combination helper
_.memoize.Cache = WeakMap;
_.combinations = _.memoize((arr, k) => {
const result = [];
function combine(start, combo) {
if (combo.length === k) {
result.push([...combo]);
return;
}
for (let i = start; i < arr.length; i++) {
combo.push(arr[i]);
combine(i + 1, combo);
combo.pop();
}
}
combine(0, []);
return result;
});
// 키보드 이벤트 핸들러 추가
useEffect(() => {
const handleKeyDown = (e) => {
if (!data.length) return; // 데이터가 없으면 무시
switch(e.key) {
case 'ArrowRight':
handleRightSwipe();
break;
case 'ArrowLeft':
handleLeftSwipe();
break;
case 'ArrowUp':
handleUpSwipe();
break;
case 'ArrowDown':
handleDownSwipe();
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [data, selectedAttributes]); // 의존성 배열 추가
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
if (!file.name.endsWith('.csv')) {
setError('Please upload a CSV file');
return;
}
setFile(file);
setIsLoading(true);
setError(null);
try {
const text = await file.text();
Papa.parse(text, {
header: true,
dynamicTyping: true,
skipEmptyLines: true,
complete: (results) => {
if (results.errors.length > 0) {
setError('Error parsing CSV file');
console.error(results.errors);
return;
}
setData(results.data);
setAttributes(Object.keys(results.data[0] || {}));
setSelectedAttributes([]);
},
error: (error) => {
setError('Error parsing CSV file');
console.error(error);
}
});
} catch (error) {
setError('Error reading file');
console.error(error);
} finally {
setIsLoading(false);
}
};
const handleTouchStart = (e) => {
setTouchStart({
x: e.touches[0].clientX,
y: e.touches[0].clientY
});
};
const handleTouchEnd = (e) => {
if (!touchStart) return;
const touchEndPos = {
x: e.changedTouches[0].clientX,
y: e.changedTouches[0].clientY
};
setTouchEnd(touchEndPos);
const dx = touchEndPos.x - touchStart.x;
const dy = touchEndPos.y - touchStart.y;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx > 50) handleRightSwipe();
else if (dx < -50) handleLeftSwipe();
} else {
if (dy > 50) handleDownSwipe();
else if (dy < -50) handleUpSwipe();
}
};
const handleRightSwipe = () => {
setChartOrientation(prev => prev === 'vertical' ? 'horizontal' : 'vertical');
};
const handleLeftSwipe = () => {
const currentIndex = CHART_TYPES.indexOf(chartType);
const nextIndex = (currentIndex + 1) % CHART_TYPES.length;
setChartType(CHART_TYPES[nextIndex]);
};
const handleUpSwipe = () => {
const unusedAttributes = attributes.filter(attr => !selectedAttributes.includes(attr));
if (unusedAttributes.length > 0) {
const mmiScores = unusedAttributes.map(attr => {
const mmi = calculateMMI([...selectedAttributes, attr]);
return { attribute: attr, score: mmi };
});
const bestAttribute = mmiScores.reduce((max, current) =>
current.score > max.score ? current : max
, mmiScores[0]);
setSelectedAttributes([...selectedAttributes, bestAttribute.attribute]);
}
};
const handleDownSwipe = () => {
if (selectedAttributes.length > 1) {
const subsets = selectedAttributes.map((_, index) => {
const subset = selectedAttributes.filter((_, i) => i !== index);
const mmi = calculateMMI(subset);
return { subset, score: mmi };
});
const bestSubset = subsets.reduce((max, current) =>
current.score > max.score ? current : max
, subsets[0]);
setSelectedAttributes(bestSubset.subset);
}
};
const renderInitialMessage = () => {
return (
<div className="text-center p-8">
<div className="mb-4 text-lg text-gray-600">
Welcome to Data Explorer
</div>
<label className="block">
<span className="sr-only">Choose CSV file</span>
<input
type="file"
accept=".csv"
onChange={handleFileUpload}
className="block w-full text-sm text-gray-500
file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100
cursor-pointer"
/>
</label>
{error && (
<div className="mt-4 text-red-500">
{error}
</div>
)}
</div>
);
};
const renderVisualization = () => {
if (selectedAttributes.length === 0) return null;
const chartProps = {
width: 600,
height: 400,
data: data,
margin: { top: 20, right: 30, left: 20, bottom: 5 }
};
// 단일 속성일 경우 처리
if (selectedAttributes.length === 1) {
// 데이터 포인트의 개수를 계산
const values = data.map(row => row[selectedAttributes[0]]);
const processedData = _.countBy(values);
const freqData = Object.entries(processedData).map(([value, count]) => ({
value,
count
}));
switch (chartType) {
case 'line':
return (
<LineChart {...chartProps} data={freqData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="value" />
<YAxis dataKey="count" />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="count"
name={selectedAttributes[0]}
stroke="hsl(210, 70%, 50%)"
/>
</LineChart>
);
case 'bar':
default:
return (
<BarChart {...chartProps} data={freqData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="value" />
<YAxis dataKey="count" />
<Tooltip />
<Legend />
<Bar
dataKey="count"
name={selectedAttributes[0]}
fill="hsl(210, 70%, 50%)"
/>
</BarChart>
);
}
}
// 다중 속성일 경우의 기존 로직
switch (chartType) {
case 'line':
return (
<LineChart {...chartProps}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={selectedAttributes[0]} />
<YAxis />
<Tooltip />
<Legend />
{selectedAttributes.slice(1).map((attr, index) => (
<Line
key={attr}
type="monotone"
dataKey={attr}
stroke={`hsl(${(index * 137.5) % 360}, 70%, 50%)`}
/>
))}
</LineChart>
);
case 'bar':
return (
<BarChart {...chartProps}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={selectedAttributes[0]} />
<YAxis />
<Tooltip />
<Legend />
{selectedAttributes.slice(1).map((attr, index) => (
<Bar
key={attr}
dataKey={attr}
fill={`hsl(${(index * 137.5) % 360}, 70%, 50%)`}
/>
))}
</BarChart>
);
case 'scatter':
if (selectedAttributes.length >= 2) {
return (
<ScatterChart {...chartProps}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={selectedAttributes[0]} />
<YAxis dataKey={selectedAttributes[1]} />
<Tooltip />
<Legend />
<Scatter
name={`${selectedAttributes[0]} vs ${selectedAttributes[1]}`}
data={data}
fill="#8884d8"
/>
</ScatterChart>
);
} else {
// 산점도는 2개 이상의 속성이 필요하므로 막대 차트로 폴백
return (
<BarChart {...chartProps}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={selectedAttributes[0]} />
<YAxis />
<Tooltip />
<Legend />
<Bar
dataKey={selectedAttributes[0]}
fill="hsl(210, 70%, 50%)"
/>
</BarChart>
);
}
}
};
return (
<div className="w-full h-full flex flex-col items-center justify-center bg-gray-100 p-4">
<Card className="w-full max-w-4xl bg-white shadow-lg">
<CardContent className="p-6">
{isLoading ? (
<div className="flex items-center justify-center h-[500px]">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
) : !data.length ? (
renderInitialMessage()
) : (
<div>
{/* Attributes Selection - 상단에 배치 */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="text-sm font-medium text-gray-600 mb-2">Select Attributes</div>
<div className="flex flex-wrap gap-2">
{attributes.map(attr => (
<button
key={attr}
className={`px-3 py-1 rounded-full text-sm ${
selectedAttributes.includes(attr)
? 'bg-blue-500 text-white'
: 'bg-gray-200 text-gray-600'
}`}
onClick={() => {
if (selectedAttributes.includes(attr)) {
setSelectedAttributes(selectedAttributes.filter(a => a !== attr));
} else {
setSelectedAttributes([...selectedAttributes, attr]);
}
}}
>
{attr}
</button>
))}
</div>
</div>
{/* Visualization Area */}
<div className="relative w-full h-[500px] flex items-center justify-center"
ref={chartRef}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}>
<div className="absolute top-0 left-1/2 -translate-x-1/2 text-gray-400">
<ArrowUp className="w-6 h-6" />
<div className="text-sm">Add Information (↑)</div>
</div>
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 text-gray-400">
<ArrowDown className="w-6 h-6" />
<div className="text-sm">Remove Information (↓)</div>
</div>
<div className="absolute left-0 top-1/2 -translate-y-1/2 text-gray-400">
<ArrowLeft className="w-6 h-6" />
<div className="text-sm">Change Type (←)</div>
</div>
<div className="absolute right-0 top-1/2 -translate-y-1/2 text-gray-400">
<ArrowRight className="w-6 h-6" />
<div className="text-sm">Change Orientation (→)</div>
</div>
<div className="w-full h-full flex items-center justify-center">
{renderVisualization()}
</div>
</div>
{/* Controls Hint */}
<div className="mt-4 text-sm text-gray-500 text-center">
Use arrow keys or swipe gestures to navigate
</div>
</div>
)}
</CardContent>
</Card>
{file && (
<div className="mt-4 text-sm text-gray-500">
Current file: {file.name}
<button
onClick={() => {
setFile(null);
setData([]);
setAttributes([]);
setSelectedAttributes([]);
setError(null);
}}
className="ml-2 text-red-500 hover:text-red-700"
>
Remove
</button>
</div>
)}
</div>
);
};
export default DataExplorer;
Kassel and Rohs 2017을 baseline으로 구현하고자 함
Kassel and Rohs 2017 - Immersive navigation in visualization spaces through swipe gestures and optimal attribute selection.pdf
구현 내용
데드라인 11월 10일