visual-notebook / frontend /src /components /VisualizationPanel.tsx
Sid-WC121
bugfix2 + update
8ac8526
Raw History Blame Contribute Delete
9.45 kB
import React, { useState } from 'react';
import { useSessionStore } from '../store/session';
import { useNotebookStore } from '../store/notebook';
import { CHART_TYPES, getChartType, isChartAvailable, countColumnTypes } from '../lib/chartTypes';
export function VisualizationPanel() {
const { columns } = useSessionStore();
const { addCell, updateCell } = useNotebookStore();
const [selectedType, setSelectedType] = useState<string>('');
const [params, setParams] = useState<Record<string, any>>({});
const colNames = columns.map((c) => c.name);
const numericCols = columns.filter((c) => c.column_type === 'numeric').map((c) => c.name);
const categoricalCols = columns.filter((c) => c.column_type === 'categorical').map((c) => c.name);
const datetimeCols = columns.filter((c) => c.column_type === 'datetime').map((c) => c.name);
const geoCols = columns.filter((c) => c.column_type === 'geo_lat_lon').map((c) => c.name);
const typeCounts = countColumnTypes(columns);
const chartType = getChartType(selectedType);
const handleCreate = () => {
if (!selectedType) return;
const cellId = addCell(selectedType === 'map' ? 'map' : 'chart', `${selectedType} chart`, {
chartType: selectedType,
params,
});
updateCell(cellId, { config: { chartType: selectedType, params } });
};
const renderColumnSelect = (name: string, relevantCols: string[], required: boolean) => (
<div key={name}>
<label className="block text-xs font-medium text-gray-600 mb-1">
{name} {required && <span className="text-red-500">*</span>}
</label>
<select
className="input-field"
value={params[name] || ''}
onChange={(e) => setParams({ ...params, [name]: e.target.value })}
>
<option value="">Select column...</option>
{relevantCols.map((c) => (
<option key={c} value={c}>{c}</option>
))}
{relevantCols.length === 0 && colNames.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
);
const getRelevantCols = (param: string): string[] => {
if (param.includes('x_col') || param.includes('y_col') || param.includes('value') || param === 'column') {
return numericCols.length > 0 ? numericCols : colNames;
}
if (param === 'columns') {
return numericCols.length > 0 ? numericCols : colNames;
}
if (param.includes('category')) {
return categoricalCols.length > 0 ? categoricalCols : colNames;
}
if (param.includes('date')) {
return datetimeCols.length > 0 ? datetimeCols : colNames;
}
if (param.includes('lat') || param.includes('lon')) {
return geoCols.length > 0 ? geoCols : numericCols.length > 0 ? numericCols : colNames;
}
if (param.includes('group')) {
return categoricalCols.length > 0 ? categoricalCols : colNames;
}
return colNames;
};
// Render a multi-select for columns (used by radar chart)
const renderMultiColumnSelect = (name: string, relevantCols: string[], required: boolean) => (
<div key={name}>
<label className="block text-xs font-medium text-gray-600 mb-1">
{name} (select multiple) {required && <span className="text-red-500">*</span>}
</label>
<div className="max-h-32 overflow-y-auto border border-gray-300 rounded-lg p-2 space-y-1 bg-white">
{relevantCols.map((c) => {
const selected = (params[name] || []).includes(c);
return (
<label key={c} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-gray-50 px-1 py-0.5 rounded">
<input
type="checkbox"
checked={selected}
onChange={(e) => {
const current: string[] = params[name] || [];
const next = e.target.checked
? [...current, c]
: current.filter((x: string) => x !== c);
setParams({ ...params, [name]: next });
}}
className="rounded border-gray-300"
/>
<span>{c}</span>
</label>
);
})}
{relevantCols.length === 0 && (
<p className="text-xs text-gray-400 italic">No columns available</p>
)}
</div>
{(params[name] || []).length > 0 && (
<p className="text-[10px] text-gray-400 mt-1">{(params[name] || []).length} selected</p>
)}
</div>
);
return (
<div className="p-4">
<h4 className="text-sm font-semibold text-gray-900 mb-3">Create Visualization</h4>
{/* Chart type grid */}
<div className="grid grid-cols-2 gap-1.5 mb-4">
{CHART_TYPES.map((ct) => {
const available = isChartAvailable(ct, columns);
return (
<div key={ct.key} className="relative group">
<button
onClick={() => {
if (available) {
setSelectedType(ct.key);
setParams({});
}
}}
disabled={!available}
className={`w-full p-2 rounded-lg border text-left transition-colors ${
selectedType === ct.key
? 'border-brand-500 bg-brand-50 text-brand-700'
: available
? 'border-gray-200 hover:border-gray-300 text-gray-600'
: 'border-gray-100 bg-gray-50 text-gray-300 cursor-not-allowed'
}`}
>
<div className="font-medium text-xs">{ct.label}</div>
<div className="text-[10px] text-gray-400 mt-0.5">{ct.description}</div>
</button>
{!available && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 bg-gray-800 text-white text-[10px] rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10">
{ct.unavailableMessage}
</div>
)}
</div>
);
})}
</div>
{/* Available types summary */}
<div className="mb-3 text-[10px] text-gray-400 flex flex-wrap gap-x-3 gap-y-0.5">
{typeCounts.numeric > 0 && <span># Numeric: {typeCounts.numeric}</span>}
{typeCounts.categorical > 0 && <span>Aa Categorical: {typeCounts.categorical}</span>}
{typeCounts.datetime > 0 && <span>DT Datetime: {typeCounts.datetime}</span>}
{typeCounts.geo_lat_lon > 0 && <span>G Geo: {typeCounts.geo_lat_lon}</span>}
</div>
{/* Parameter form */}
{chartType && (
<div className="space-y-3 mb-4">
{chartType.requiredParams.map((param) => {
if (param === 'columns') {
const relevantCols = getRelevantCols(param);
return renderMultiColumnSelect(param, relevantCols, true);
}
const relevantCols = getRelevantCols(param);
return renderColumnSelect(param, relevantCols, true);
})}
{chartType.optionalParams.map((param) => {
if (param === 'color_col' || param === 'label_col' || param === 'group_col') {
return renderColumnSelect(param, colNames, false);
}
if (param === 'bins' || param === 'n') {
return (
<div key={param}>
<label className="block text-xs font-medium text-gray-600 mb-1">{param}</label>
<input
type="number"
className="input-field"
value={params[param] || (param === 'bins' ? 20 : 15)}
onChange={(e) => setParams({ ...params, [param]: parseInt(e.target.value) || 0 })}
/>
</div>
);
}
if (param === 'sort_desc') {
return (
<label key={param} className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={params[param] !== false}
onChange={(e) => setParams({ ...params, [param]: e.target.checked })}
className="rounded border-gray-300"
/>
Sort descending
</label>
);
}
if (param === 'agg') {
return (
<div key={param}>
<label className="block text-xs font-medium text-gray-600 mb-1">Aggregation</label>
<select
className="input-field"
value={params[param] || 'sum'}
onChange={(e) => setParams({ ...params, [param]: e.target.value })}
>
<option value="sum">Sum</option>
<option value="mean">Mean</option>
<option value="count">Count</option>
<option value="min">Min</option>
<option value="max">Max</option>
<option value="median">Median</option>
</select>
</div>
);
}
return null;
})}
</div>
)}
<button
onClick={handleCreate}
disabled={!selectedType}
className="btn-primary w-full"
>
Create Chart
</button>
</div>
);
}