Spaces:
Sleeping
Sleeping
Download frontend/src/components/VisualizationPanel.tsx from Sid-WC121/visual-notebook: direct link, hf CLI and curl.
- Browser
- Download file 9.45 kB
-
https://huggingface.co/spaces/Sid-WC121/visual-notebook/resolve/main/frontend/src/components/VisualizationPanel.tsx
- Command line
-
hf download hf://spaces/Sid-WC121/visual-notebook/frontend/src/components/VisualizationPanel.tsx
-
curl -L -o VisualizationPanel.tsx https://huggingface.co/spaces/Sid-WC121/visual-notebook/resolve/main/frontend/src/components/VisualizationPanel.tsx
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> | |
| ); | |
| } | |