mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-12 23:01:21 +00:00
Adding Project using Chat
This commit is contained in:
parent
4d749962fb
commit
bde397c93a
31 changed files with 10102 additions and 1656 deletions
39
apps/web-roo-code/src/components/excel-analyzer/CLAUDE.md
Normal file
39
apps/web-roo-code/src/components/excel-analyzer/CLAUDE.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# CLAUDE.md — Shared Brain / Lessons Learned
|
||||
|
||||
Durable project memory for architecture, structure, and workflow decisions.
|
||||
|
||||
## Current Snapshot
|
||||
|
||||
- Project: **Excel Analyzer**
|
||||
- Runtime: Browser-only (client-side)
|
||||
- Extensibility: `src/hooks/hook-engine.js`
|
||||
- Operational memory:
|
||||
- `.orchestration/active_intents.yaml`
|
||||
- `.orchestration/agent_trace.jsonl`
|
||||
- `.orchestration/intent_map.md`
|
||||
|
||||
## Documentation Map
|
||||
|
||||
- `README.md` (workspace root)
|
||||
- `Excel-Based-Data-Analzer/README.md` (application-level guide)
|
||||
- `specify/specification.md`
|
||||
- `specify/memory/constitution.md`
|
||||
- `specify/plan/plan.md`
|
||||
- `specify/architecture/architecture.md`
|
||||
- `specify/project_structure/project_structure.md`
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. Keep structure docs synchronized immediately after file moves.
|
||||
2. Keep hook-specific logic isolated under `src/hooks/`.
|
||||
3. Keep orchestration artifacts lightweight and plain-text for traceability.
|
||||
4. Update both root and app README when structure changes.
|
||||
|
||||
## Working Agreements
|
||||
|
||||
- `.orchestration/active_intents.yaml` tracks active goals.
|
||||
- `.orchestration/agent_trace.jsonl` logs key operations.
|
||||
- `.orchestration/intent_map.md` maps goals to code areas.
|
||||
- `specify/project_structure/project_structure.md` is the canonical structure reference.
|
||||
|
||||
|
||||
72
apps/web-roo-code/src/components/excel-analyzer/README.md
Normal file
72
apps/web-roo-code/src/components/excel-analyzer/README.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Excel Analyzer Project
|
||||
|
||||
Web-based Excel analysis application with modular architecture, local-first processing, and extensibility via a dedicated Hook Engine.
|
||||
|
||||
## What It Does
|
||||
|
||||
- Upload and parse Excel files (`.xlsx`, `.xls`)
|
||||
- Preview tabular data with basic quality indicators
|
||||
- Analyze/visualize data (Chart.js)
|
||||
- View a compact dashboard summary
|
||||
- Export results (PDF / Excel / CSV / JSON / PNG)
|
||||
|
||||
## Architecture Highlights
|
||||
|
||||
- **Client-side only** processing for privacy
|
||||
- **Module-oriented JS structure** under `src/js/`
|
||||
- **Hook Engine** under `src/hooks/hook-engine.js` for extensibility
|
||||
- **Project memory and orchestration artifacts**:
|
||||
- `CLAUDE.md`
|
||||
- `.orchestration/active_intents.yaml`
|
||||
- `.orchestration/agent_trace.jsonl`
|
||||
- `.orchestration/intent_map.md`
|
||||
|
||||
## Current Structure
|
||||
|
||||
```text
|
||||
Week1_TRP1_Intent_based_planning/
|
||||
├── README.md
|
||||
├── CLAUDE.md
|
||||
├── .orchestration/
|
||||
│ ├── active_intents.yaml
|
||||
│ ├── agent_trace.jsonl
|
||||
│ └── intent_map.md
|
||||
├── specify/
|
||||
│ ├── specification.md
|
||||
│ ├── memory/
|
||||
│ │ └── constitution.md
|
||||
│ ├── plan/
|
||||
│ │ └── plan.md
|
||||
│ ├── architecture/
|
||||
│ │ └── architecture.md
|
||||
│ └── project_structure/
|
||||
│ └── project_structure.md
|
||||
└── src/
|
||||
├── index.html
|
||||
├── css/
|
||||
│ └── styles.css
|
||||
├── hooks/
|
||||
│ └── hook-engine.js
|
||||
└── js/
|
||||
├── main.js
|
||||
├── config/
|
||||
├── events/
|
||||
├── modules/
|
||||
└── utils/
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Open in browser:
|
||||
|
||||
```powershell
|
||||
start Excel-Based-Data-Analzer/src/index.html
|
||||
```
|
||||
|
||||
## Related Docs
|
||||
|
||||
- Specification: `specify/specification.md`
|
||||
- Constitution: `specify/memory/constitution.md`
|
||||
- Plan: `specify/plan/plan.md`
|
||||
- Architecture: `specify/architecture/architecture.md`
|
||||
- Project Structure: `specify/project_structure/project_structure.md`
|
||||
|
|
@ -1,484 +0,0 @@
|
|||
# Excel Analyzer - Component Specifications
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### 1. UploadComponent
|
||||
|
||||
**Location**: `src/components/excel-analyzer/UploadComponent.tsx`
|
||||
|
||||
**Purpose**: Handle Excel file uploads with drag-and-drop interface
|
||||
|
||||
**Features**:
|
||||
|
||||
- Drag-and-drop file upload area
|
||||
- File type validation (.xlsx, .csv)
|
||||
- File size limits enforcement
|
||||
- Progress tracking
|
||||
- Error handling and user feedback
|
||||
- Preview of uploaded file structure
|
||||
|
||||
**Props**:
|
||||
|
||||
```typescript
|
||||
interface UploadComponentProps {
|
||||
onFileUploaded: (file: File) => void
|
||||
onError: (error: string) => void
|
||||
maxFileSize?: number // in bytes
|
||||
acceptedFormats?: string[]
|
||||
}
|
||||
```
|
||||
|
||||
**State**:
|
||||
|
||||
- `isDragging: boolean`
|
||||
- `uploadProgress: number`
|
||||
- `selectedFile: File | null`
|
||||
- `error: string | null`
|
||||
|
||||
### 2. IntentInputComponent
|
||||
|
||||
**Location**: `src/components/excel-analyzer/IntentInputComponent.tsx`
|
||||
|
||||
**Purpose**: Capture user's analysis intent through natural language input
|
||||
|
||||
**Features**:
|
||||
|
||||
- Text input area for analysis requests
|
||||
- AI-powered intent recognition
|
||||
- Parameter extraction
|
||||
- Context understanding
|
||||
- Real-time suggestions
|
||||
- Error handling for invalid requests
|
||||
|
||||
**Props**:
|
||||
|
||||
```typescript
|
||||
interface IntentInputComponentProps {
|
||||
onIntentRecognized: (intent: AnalysisIntent) => void
|
||||
onError: (error: string) => void
|
||||
placeholder?: string
|
||||
}
|
||||
```
|
||||
|
||||
**State**:
|
||||
|
||||
- `userInput: string`
|
||||
- `recognizedIntent: AnalysisIntent | null`
|
||||
- `processing: boolean`
|
||||
- `error: string | null`
|
||||
|
||||
### 3. DashboardComponent
|
||||
|
||||
**Location**: `src/components/excel-analyzer/DashboardComponent.tsx`
|
||||
|
||||
**Purpose**: Display analysis results and insights
|
||||
|
||||
**Features**:
|
||||
|
||||
- Real-time progress tracking
|
||||
- Interactive charts and graphs
|
||||
- Data tables with sorting/filtering
|
||||
- Summary statistics
|
||||
- Export controls
|
||||
- Multiple view modes (table, chart, summary)
|
||||
|
||||
**Props**:
|
||||
|
||||
```typescript
|
||||
interface DashboardComponentProps {
|
||||
analysisResults: AnalysisResults
|
||||
onExport: (format: ExportFormat) => void
|
||||
onViewChange: (view: "table" | "chart" | "summary") => void
|
||||
}
|
||||
```
|
||||
|
||||
**State**:
|
||||
|
||||
- `currentView: 'table' | 'chart' | 'summary'`
|
||||
- `selectedData: any[]`
|
||||
- `filters: FilterOptions`
|
||||
- `sort: SortOptions`
|
||||
|
||||
### 4. ExportComponent
|
||||
|
||||
**Location**: `src/components/excel-analyzer/ExportComponent.tsx`
|
||||
|
||||
**Purpose**: Handle result export in multiple formats
|
||||
|
||||
**Features**:
|
||||
|
||||
- Format selection (Excel, PDF, CSV, JSON)
|
||||
- Export progress tracking
|
||||
- Download management
|
||||
- Format-specific options
|
||||
- Error handling
|
||||
|
||||
**Props**:
|
||||
|
||||
```typescript
|
||||
interface ExportComponentProps {
|
||||
analysisResults: AnalysisResults
|
||||
onExport: (format: ExportFormat, options?: ExportOptions) => void
|
||||
onDownload: (fileUrl: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
**State**:
|
||||
|
||||
- `selectedFormat: ExportFormat`
|
||||
- `exportProgress: number`
|
||||
- `exportOptions: ExportOptions`
|
||||
- `downloadUrl: string | null`
|
||||
|
||||
### 5. ProgressComponent
|
||||
|
||||
**Location**: `src/components/excel-analyzer/ProgressComponent.tsx`
|
||||
|
||||
**Purpose**: Display overall workflow progress
|
||||
|
||||
**Features**:
|
||||
|
||||
- Multi-step progress indicator
|
||||
- Current step highlighting
|
||||
- Estimated time remaining
|
||||
- Pause/resume functionality
|
||||
- Error recovery options
|
||||
|
||||
**Props**:
|
||||
|
||||
```typescript
|
||||
interface ProgressComponentProps {
|
||||
currentStep: number
|
||||
totalSteps: number
|
||||
stepLabels: string[]
|
||||
progress: number
|
||||
status: "pending" | "processing" | "completed" | "error"
|
||||
onRetry?: () => void
|
||||
onPause?: () => void
|
||||
onResume?: () => void
|
||||
}
|
||||
```
|
||||
|
||||
**State**:
|
||||
|
||||
- `currentStep: number`
|
||||
- `progress: number`
|
||||
- `status: 'pending' | 'processing' | 'completed' | 'error'`
|
||||
- `error: string | null`
|
||||
|
||||
## Service Specifications
|
||||
|
||||
### 1. FileUploadService
|
||||
|
||||
**Location**: `src/lib/fileUploadService.ts`
|
||||
|
||||
**Purpose**: Handle file upload and validation
|
||||
|
||||
**Methods**:
|
||||
|
||||
```typescript
|
||||
interface FileUploadService {
|
||||
uploadFile(file: File): Promise<UploadedFile>
|
||||
validateFile(file: File): ValidationResult
|
||||
getFilePreview(file: File): Promise<FilePreview>
|
||||
getFileSize(file: File): number
|
||||
}
|
||||
```
|
||||
|
||||
### 2. AIService
|
||||
|
||||
**Location**: `src/lib/aiService.ts`
|
||||
|
||||
**Purpose**: Handle AI intent recognition and analysis
|
||||
|
||||
**Methods**:
|
||||
|
||||
```typescript
|
||||
interface AIService {
|
||||
recognizeIntent(userInput: string): Promise<AnalysisIntent>
|
||||
extractParameters(intent: AnalysisIntent): ParameterSet
|
||||
analyzeData(data: any[], intent: AnalysisIntent): Promise<AnalysisResults>
|
||||
}
|
||||
```
|
||||
|
||||
### 3. DataProcessingService
|
||||
|
||||
**Location**: `src/lib/dataProcessingService.ts`
|
||||
|
||||
**Purpose**: Handle data preprocessing and transformation
|
||||
|
||||
**Methods**:
|
||||
|
||||
```typescript
|
||||
interface DataProcessingService {
|
||||
preprocessData(data: any[]): ProcessedData
|
||||
cleanData(data: any[]): CleanedData
|
||||
transformData(data: any[], transformation: Transformation): TransformedData
|
||||
validateSchema(data: any[], schema: Schema): ValidationResult
|
||||
}
|
||||
```
|
||||
|
||||
### 4. ExportService
|
||||
|
||||
**Location**: `src/lib/exportService.ts`
|
||||
|
||||
**Purpose**: Handle result export in multiple formats
|
||||
|
||||
**Methods**:
|
||||
|
||||
```typescript
|
||||
interface ExportService {
|
||||
exportToExcel(data: any[], options?: ExportOptions): Promise<Blob>
|
||||
exportToPDF(data: any[], options?: ExportOptions): Promise<Blob>
|
||||
exportToCSV(data: any[], options?: ExportOptions): Promise<Blob>
|
||||
exportToJSON(data: any[], options?: ExportOptions): Promise<Blob>
|
||||
downloadFile(blob: Blob, filename: string): void
|
||||
}
|
||||
```
|
||||
|
||||
### 5. OrchestrationService
|
||||
|
||||
**Location**: `src/lib/orchestrationService.ts`
|
||||
|
||||
**Purpose**: Manage workflow and coordinate services
|
||||
|
||||
**Methods**:
|
||||
|
||||
```typescript
|
||||
interface OrchestrationService {
|
||||
startWorkflow(file: File, intent: AnalysisIntent): Promise<WorkflowResult>
|
||||
pauseWorkflow(workflowId: string): Promise<void>
|
||||
resumeWorkflow(workflowId: string): Promise<void>
|
||||
cancelWorkflow(workflowId: string): Promise<void>
|
||||
getWorkflowStatus(workflowId: string): Promise<WorkflowStatus>
|
||||
}
|
||||
```
|
||||
|
||||
## Hook Specifications
|
||||
|
||||
### 1. useFileUpload
|
||||
|
||||
**Location**: `src/hooks/useFileUpload.ts`
|
||||
|
||||
**Purpose**: Manage file upload state and logic
|
||||
|
||||
**Returns**:
|
||||
|
||||
```typescript
|
||||
interface UseFileUploadReturn {
|
||||
isDragging: boolean
|
||||
uploadProgress: number
|
||||
selectedFile: File | null
|
||||
error: string | null
|
||||
handleDragEnter: () => void
|
||||
handleDragLeave: () => void
|
||||
handleDrop: (files: FileList) => void
|
||||
handleFileSelect: (file: File) => void
|
||||
uploadFile: () => Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### 2. useIntentRecognition
|
||||
|
||||
**Location**: `src/hooks/useIntentRecognition.ts`
|
||||
|
||||
**Purpose**: Manage intent recognition state and logic
|
||||
|
||||
**Returns**:
|
||||
|
||||
```typescript
|
||||
interface UseIntentRecognitionReturn {
|
||||
userInput: string
|
||||
recognizedIntent: AnalysisIntent | null
|
||||
processing: boolean
|
||||
error: string | null
|
||||
handleInputChange: (input: string) => void
|
||||
recognizeIntent: () => Promise<void>
|
||||
clearIntent: () => void
|
||||
}
|
||||
```
|
||||
|
||||
### 3. useDataProcessing
|
||||
|
||||
**Location**: `src/hooks/useDataProcessing.ts`
|
||||
|
||||
**Purpose**: Manage data processing state and logic
|
||||
|
||||
**Returns**:
|
||||
|
||||
```typescript
|
||||
interface UseDataProcessingReturn {
|
||||
processedData: ProcessedData | null
|
||||
processing: boolean
|
||||
error: string | null
|
||||
processData: (data: any[]) => Promise<void>
|
||||
cleanData: (data: any[]) => Promise<void>
|
||||
transformData: (data: any[], transformation: Transformation) => Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### 4. useExport
|
||||
|
||||
**Location**: `src/hooks/useExport.ts`
|
||||
|
||||
**Purpose**: Manage export state and logic
|
||||
|
||||
**Returns**:
|
||||
|
||||
```typescript
|
||||
interface UseExportReturn {
|
||||
selectedFormat: ExportFormat
|
||||
exportProgress: number
|
||||
exportOptions: ExportOptions
|
||||
downloadUrl: string | null
|
||||
handleFormatChange: (format: ExportFormat) => void
|
||||
handleExport: () => Promise<void>
|
||||
handleDownload: () => void
|
||||
clearExport: () => void
|
||||
}
|
||||
```
|
||||
|
||||
## Type Specifications
|
||||
|
||||
### File Types
|
||||
|
||||
```typescript
|
||||
interface FileTypes {
|
||||
UploadedFile: {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
type: string
|
||||
preview: FilePreview
|
||||
}
|
||||
|
||||
FilePreview: {
|
||||
columns: string[]
|
||||
sampleData: any[][]
|
||||
rowCount: number
|
||||
columnTypes: Record<string, string>
|
||||
}
|
||||
|
||||
ValidationResult: {
|
||||
isValid: boolean
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Analysis Types
|
||||
|
||||
```typescript
|
||||
interface AnalysisTypes {
|
||||
AnalysisIntent: {
|
||||
type: "descriptive" | "predictive" | "diagnostic" | "prescriptive"
|
||||
parameters: Record<string, any>
|
||||
context: string
|
||||
}
|
||||
|
||||
AnalysisResults: {
|
||||
summary: AnalysisSummary
|
||||
charts: ChartData[]
|
||||
tables: DataTable[]
|
||||
insights: Insight[]
|
||||
recommendations: Recommendation[]
|
||||
}
|
||||
|
||||
AnalysisSummary: {
|
||||
metrics: Record<string, number>
|
||||
trends: Trend[]
|
||||
patterns: Pattern[]
|
||||
anomalies: Anomaly[]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Export Types
|
||||
|
||||
```typescript
|
||||
interface ExportTypes {
|
||||
ExportFormat: "excel" | "pdf" | "csv" | "json"
|
||||
|
||||
ExportOptions: {
|
||||
includeCharts: boolean
|
||||
includeTables: boolean
|
||||
includeInsights: boolean
|
||||
formatOptions: Record<string, any>
|
||||
}
|
||||
|
||||
ExportProgress: {
|
||||
totalSteps: number
|
||||
currentStep: number
|
||||
stepName: string
|
||||
percentage: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### 1. Component-Service Integration
|
||||
|
||||
- UploadComponent → FileUploadService
|
||||
- IntentInputComponent → AIService
|
||||
- DashboardComponent → DataProcessingService
|
||||
- ExportComponent → ExportService
|
||||
- ProgressComponent → OrchestrationService
|
||||
|
||||
### 2. Hook-Service Integration
|
||||
|
||||
- useFileUpload → FileUploadService
|
||||
- useIntentRecognition → AIService
|
||||
- useDataProcessing → DataProcessingService
|
||||
- useExport → ExportService
|
||||
|
||||
### 3. State Management
|
||||
|
||||
- Global state for workflow management
|
||||
- Local state for component-specific data
|
||||
- Context providers for shared services
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
### 1. Component-Level Errors
|
||||
|
||||
- User-friendly error messages
|
||||
- Retry mechanisms
|
||||
- Fallback options
|
||||
|
||||
### 2. Service-Level Errors
|
||||
|
||||
- Error logging and monitoring
|
||||
- Graceful degradation
|
||||
- Circuit breaker patterns
|
||||
|
||||
### 3. Workflow Errors
|
||||
|
||||
- Transaction rollback
|
||||
- State recovery
|
||||
- User notification
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. File Processing
|
||||
|
||||
- Streaming for large files
|
||||
- Web Workers for heavy processing
|
||||
- Caching for repeated operations
|
||||
|
||||
### 2. AI Processing
|
||||
|
||||
- Debouncing for user input
|
||||
- Caching for common intents
|
||||
- Background processing
|
||||
|
||||
### 3. UI Rendering
|
||||
|
||||
- Virtual scrolling for large datasets
|
||||
- Memoization for expensive computations
|
||||
- Lazy loading for components
|
||||
|
||||
---
|
||||
|
||||
_This component specification provides a detailed blueprint for implementing the Excel Analyzer functionality within the existing web-roo-code app structure._
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Excel Analyzer</title>
|
||||
<link href="https://cdn.tailwindcss.com" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/papaparse/5.3.1/papaparse.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
||||
<style>
|
||||
.hidden-file-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.spinner {
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold text-center mb-8 text-gray-800">Excel Analyzer</h1>
|
||||
|
||||
<!-- Upload Component -->
|
||||
<div id="uploadComponent" class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-700">Upload Excel File</h2>
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<div id="uploadArea" class="border-2 border-dashed rounded-lg p-8 text-center transition-all">
|
||||
<div class="text-4xl text-gray-400 mb-4">
|
||||
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-700 mb-2">Upload Excel File</h3>
|
||||
<p class="text-gray-500 mb-4">Drag and drop your Excel file here or click to browse</p>
|
||||
<p class="text-sm text-gray-500 mb-4">Supported formats: .xlsx, .csv</p>
|
||||
|
||||
<input type="file" id="fileInput" class="hidden-file-input" accept=".xlsx,.csv">
|
||||
<label for="fileInput" class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg cursor-pointer hover:bg-blue-700 transition-colors">
|
||||
Browse Files
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- File Preview -->
|
||||
<div id="filePreview" class="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg hidden">
|
||||
<h3 class="text-lg font-semibold mb-2">File Preview</h3>
|
||||
<div class="space-y-2">
|
||||
<p><strong>Name:</strong> <span id="previewName"></span></p>
|
||||
<p><strong>Size:</strong> <span id="previewSize"></span> KB</p>
|
||||
<p><strong>Columns:</strong> <span id="previewColumns"></span></p>
|
||||
<p><strong>Rows:</strong> <span id="previewRows"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected File Info -->
|
||||
<div id="selectedFileInfo" class="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg hidden">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h4 class="font-medium">Selected File:</h4>
|
||||
<p class="text-sm text-gray-600" id="selectedFileName"></p>
|
||||
</div>
|
||||
<button id="removeFileBtn" class="text-red-600 hover:text-red-700">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div id="errorMessage" class="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 hidden"></div>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<div id="uploadButton" class="mt-4 hidden">
|
||||
<button id="uploadFileBtn" class="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed">
|
||||
Upload File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intent Input Component -->
|
||||
<div id="intentInputComponent" class="mb-8 hidden">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-700">Analyze Your Data</h2>
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<!-- Processing Indicator -->
|
||||
<div id="processingIndicator" class="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg text-blue-700 flex items-center justify-between hidden">
|
||||
<div class="flex items-center">
|
||||
<div class="spinner"></div>
|
||||
<span>Analyzing your request...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recognized Intent -->
|
||||
<div id="recognizedIntent" class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg hidden">
|
||||
<h4 class="font-medium mb-2">Recognized Intent:</h4>
|
||||
<div class="space-y-2">
|
||||
<p><strong>Type:</strong> <span id="intentType"></span></p>
|
||||
<p><strong>Context:</strong> <span id="intentContext"></span></p>
|
||||
<p><strong>Parameters:</strong> <span id="intentParameters"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Suggestions -->
|
||||
<div id="suggestions" class="mb-4 p-3 bg-gray-50 border border-gray-200 rounded-lg hidden">
|
||||
<h5 class="font-medium mb-2">Similar Requests:</h5>
|
||||
<div id="suggestionsList" class="flex flex-wrap gap-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="mb-4">
|
||||
<textarea id="userInput" rows="4" placeholder="Describe what you want to analyze..." class="w-full p-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-between items-center">
|
||||
<button id="clearIntentBtn" class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
||||
Clear
|
||||
</button>
|
||||
|
||||
<button id="analyzeBtn" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed">
|
||||
Analyze
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Component -->
|
||||
<div id="dashboardComponent" class="mb-8 hidden">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-700">Analysis Results</h2>
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<!-- View Selector -->
|
||||
<div class="mb-4 flex space-x-2">
|
||||
<button id="chartViewBtn" class="px-4 py-2 rounded-lg transition-colors bg-blue-600 text-white">
|
||||
Chart
|
||||
</button>
|
||||
<button id="tableViewBtn" class="px-4 py-2 rounded-lg transition-colors bg-gray-200 text-gray-700 hover:bg-gray-300">
|
||||
Table
|
||||
</button>
|
||||
<button id="summaryViewBtn" class="px-4 py-2 rounded-lg transition-colors bg-gray-200 text-gray-700 hover:bg-gray-300">
|
||||
Summary
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Export Controls -->
|
||||
<div class="mb-4 flex space-x-2">
|
||||
<button id="exportExcelBtn" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
Export to Excel
|
||||
</button>
|
||||
<button id="exportPDFBtn" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">
|
||||
Export to PDF
|
||||
</button>
|
||||
<button id="exportCSVBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
Export to CSV
|
||||
</button>
|
||||
<button id="exportJSONBtn" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
||||
Export to JSON
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div id="mainContent" class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<p>No analysis results available. Please upload a file and perform analysis.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="excel-analyzer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,988 +0,0 @@
|
|||
// Excel Analyzer - JavaScript Implementation
|
||||
class FileUploadService {
|
||||
constructor() {
|
||||
this.maxFileSize = 10 * 1024 * 1024 // 10MB
|
||||
}
|
||||
|
||||
validateFile(file) {
|
||||
const errors = []
|
||||
const warnings = []
|
||||
|
||||
// Check file size
|
||||
if (file.size > this.maxFileSize) {
|
||||
errors.push(`File size exceeds limit of ${this.maxFileSize / (1024 * 1024)}MB`)
|
||||
}
|
||||
|
||||
// Check file type
|
||||
const fileType = file.type.toLowerCase()
|
||||
const fileName = file.name.toLowerCase()
|
||||
const isExcel = fileType.includes("sheet") || fileType.includes("spreadsheet") || fileName.endsWith(".xlsx")
|
||||
const isCSV = fileType.includes("csv") || fileName.endsWith(".csv")
|
||||
|
||||
if (!isExcel && !isCSV) {
|
||||
errors.push("Invalid file format. Only .xlsx and .csv files are supported.")
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
async getFilePreview(file) {
|
||||
const data = await this.parseFile(file)
|
||||
const firstRow = data[0] || []
|
||||
const sampleData = data.slice(1, 6) // First 5 rows of data
|
||||
|
||||
// Infer column types
|
||||
const columnTypes = {}
|
||||
firstRow.forEach((col, index) => {
|
||||
const sampleValues = data.slice(1, 20).map((row) => row[index]) // Sample 20 rows
|
||||
const types = new Set(sampleValues.map(this.inferType))
|
||||
columnTypes[col] = Array.from(types).join(" | ")
|
||||
})
|
||||
|
||||
return {
|
||||
columns: firstRow,
|
||||
sampleData,
|
||||
rowCount: data.length - 1, // Exclude header row
|
||||
columnTypes,
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(file) {
|
||||
const data = await this.parseFile(file)
|
||||
const preview = await this.getFilePreview(file)
|
||||
|
||||
return {
|
||||
id: this.generateId(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
preview,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
async parseFile(file) {
|
||||
const fileType = file.name.toLowerCase().split(".").pop()
|
||||
|
||||
if (fileType === "xlsx") {
|
||||
return this.parseExcel(file)
|
||||
} else if (fileType === "csv") {
|
||||
return this.parseCSV(file)
|
||||
} else {
|
||||
throw new Error("Unsupported file format")
|
||||
}
|
||||
}
|
||||
|
||||
async parseExcel(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
if (!e.target?.result) {
|
||||
reject(new Error("Failed to read file"))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const data = new Uint8Array(e.target.result)
|
||||
const workbook = XLSX.read(data, { type: "array" })
|
||||
const firstSheetName = workbook.SheetNames[0]
|
||||
const worksheet = workbook.Sheets[firstSheetName]
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
|
||||
|
||||
resolve(jsonData)
|
||||
} catch (error) {
|
||||
reject(new Error("Failed to parse Excel file"))
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Failed to read file"))
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
async parseCSV(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = (e) => {
|
||||
if (!e.target?.result) {
|
||||
reject(new Error("Failed to read file"))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const csvData = e.target.result
|
||||
const result = Papa.parse(csvData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
dynamicTyping: true,
|
||||
})
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
reject(new Error(result.errors.map((e) => e.message).join(", ")))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to array of arrays format
|
||||
const data = []
|
||||
if (result.data.length > 0) {
|
||||
const headers = Object.keys(result.data[0])
|
||||
data.push(headers)
|
||||
result.data.forEach((row) => {
|
||||
data.push(headers.map((header) => row[header]))
|
||||
})
|
||||
}
|
||||
|
||||
resolve(data)
|
||||
} catch (error) {
|
||||
reject(new Error("Failed to parse CSV file"))
|
||||
}
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Failed to read file"))
|
||||
}
|
||||
|
||||
reader.readAsText(file)
|
||||
})
|
||||
}
|
||||
|
||||
inferType(value) {
|
||||
if (value === null || value === undefined || value === "") return "empty"
|
||||
if (!isNaN(Date.parse(value))) return "date"
|
||||
if (!isNaN(parseFloat(value)) && isFinite(value)) return "number"
|
||||
if (typeof value === "boolean" || value === "true" || value === "false") return "boolean"
|
||||
return "string"
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
}
|
||||
|
||||
class AIService {
|
||||
constructor() {
|
||||
this.openAiApiKey = "" // No API key for mock implementation
|
||||
}
|
||||
|
||||
async recognizeIntent(userInput) {
|
||||
// Mock implementation - replace with actual OpenAI API call
|
||||
const mockIntents = [
|
||||
{
|
||||
type: "descriptive",
|
||||
parameters: { columns: ["Sales", "Profit"] },
|
||||
context: "Analyze sales and profit data",
|
||||
},
|
||||
{
|
||||
type: "predictive",
|
||||
parameters: { target: "Sales", timePeriod: "next quarter" },
|
||||
context: "Forecast future sales based on historical data",
|
||||
},
|
||||
{
|
||||
type: "diagnostic",
|
||||
parameters: { issue: "declining sales", timeframe: "last 6 months" },
|
||||
context: "Investigate reasons for declining sales",
|
||||
},
|
||||
{
|
||||
type: "prescriptive",
|
||||
parameters: { goal: "increase profit", budget: 10000 },
|
||||
context: "Recommend actions to increase profit",
|
||||
},
|
||||
]
|
||||
|
||||
// Simple intent recognition based on keywords
|
||||
const lowerInput = userInput.toLowerCase()
|
||||
|
||||
if (lowerInput.includes("forecast") || lowerInput.includes("predict")) {
|
||||
return mockIntents[1] || mockIntents[0]
|
||||
} else if (lowerInput.includes("why") || lowerInput.includes("reason")) {
|
||||
return mockIntents[2] || mockIntents[0]
|
||||
} else if (lowerInput.includes("recommend") || lowerInput.includes("suggest")) {
|
||||
return mockIntents[3] || mockIntents[0]
|
||||
} else {
|
||||
return mockIntents[0]
|
||||
}
|
||||
}
|
||||
|
||||
async extractParameters(intent) {
|
||||
// Mock implementation - replace with actual parameter extraction
|
||||
const mockParameters = {
|
||||
filters: [],
|
||||
aggregations: [],
|
||||
transformations: [],
|
||||
}
|
||||
|
||||
// Add default filters based on intent type
|
||||
if (intent.type === "descriptive") {
|
||||
mockParameters.aggregations.push({
|
||||
type: "sum",
|
||||
column: "Sales",
|
||||
alias: "Total Sales",
|
||||
})
|
||||
mockParameters.aggregations.push({
|
||||
type: "average",
|
||||
column: "Profit",
|
||||
alias: "Average Profit",
|
||||
})
|
||||
}
|
||||
|
||||
if (intent.type === "predictive") {
|
||||
mockParameters.filters.push({
|
||||
column: "Date",
|
||||
operator: "greaterThan",
|
||||
value: "2023-01-01",
|
||||
})
|
||||
}
|
||||
|
||||
return mockParameters
|
||||
}
|
||||
|
||||
async analyzeData(data, intent) {
|
||||
// Mock implementation - replace with actual data analysis
|
||||
const mockResults = {
|
||||
summary: {
|
||||
metrics: {
|
||||
totalRows: data.length - 1,
|
||||
totalColumns: data[0]?.length || 0,
|
||||
completeness: 0.95,
|
||||
},
|
||||
trends: [],
|
||||
patterns: [],
|
||||
anomalies: [],
|
||||
},
|
||||
charts: [],
|
||||
tables: [],
|
||||
insights: [],
|
||||
recommendations: [],
|
||||
}
|
||||
|
||||
// Generate mock insights based on intent type
|
||||
if (intent.type === "descriptive") {
|
||||
mockResults.insights.push({
|
||||
type: "pattern",
|
||||
description: "Sales show a consistent upward trend",
|
||||
severity: "medium",
|
||||
data: { trend: "upward", confidence: 0.8 },
|
||||
})
|
||||
}
|
||||
|
||||
if (intent.type === "predictive") {
|
||||
mockResults.recommendations.push({
|
||||
type: "action",
|
||||
description: "Increase marketing budget by 10% to boost sales",
|
||||
priority: "high",
|
||||
steps: ["Review current marketing spend", "Allocate additional budget", "Monitor results"],
|
||||
})
|
||||
}
|
||||
|
||||
return mockResults
|
||||
}
|
||||
|
||||
async getSimilarRequests(intent) {
|
||||
// Mock implementation - replace with actual similar request generation
|
||||
const similarRequests = [
|
||||
"Show me the total sales by region",
|
||||
"What are the top performing products?",
|
||||
"Analyze profit margins by category",
|
||||
"Compare sales performance year over year",
|
||||
"Identify trends in customer behavior",
|
||||
]
|
||||
|
||||
return similarRequests
|
||||
}
|
||||
}
|
||||
|
||||
class ExcelAnalyzer {
|
||||
constructor() {
|
||||
this.fileUploadService = new FileUploadService()
|
||||
this.aiService = new AIService()
|
||||
this.uploadedFile = null
|
||||
this.analysisResults = null
|
||||
this.currentView = "chart"
|
||||
|
||||
this.initializeEventListeners()
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
// Upload Component Events
|
||||
const fileInput = document.getElementById("fileInput")
|
||||
const uploadArea = document.getElementById("uploadArea")
|
||||
const removeFileBtn = document.getElementById("removeFileBtn")
|
||||
const uploadFileBtn = document.getElementById("uploadFileBtn")
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", (e) => {
|
||||
const files = e.target.files
|
||||
if (files && files.length > 0) {
|
||||
this.handleFileSelect(files[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (uploadArea) {
|
||||
uploadArea.addEventListener("dragenter", (e) => this.handleDragEnter(e))
|
||||
uploadArea.addEventListener("dragleave", (e) => this.handleDragLeave(e))
|
||||
uploadArea.addEventListener("drop", (e) => this.handleDrop(e))
|
||||
uploadArea.addEventListener("dragover", (e) => e.preventDefault())
|
||||
}
|
||||
|
||||
if (removeFileBtn) {
|
||||
removeFileBtn.addEventListener("click", () => this.clearFile())
|
||||
}
|
||||
|
||||
if (uploadFileBtn) {
|
||||
uploadFileBtn.addEventListener("click", () => this.uploadFile())
|
||||
}
|
||||
|
||||
// Intent Input Component Events
|
||||
const analyzeBtn = document.getElementById("analyzeBtn")
|
||||
const clearIntentBtn = document.getElementById("clearIntentBtn")
|
||||
const userInput = document.getElementById("userInput")
|
||||
|
||||
if (analyzeBtn) {
|
||||
analyzeBtn.addEventListener("click", () => this.recognizeIntent())
|
||||
}
|
||||
|
||||
if (clearIntentBtn) {
|
||||
clearIntentBtn.addEventListener("click", () => this.clearIntent())
|
||||
}
|
||||
|
||||
if (userInput) {
|
||||
userInput.addEventListener("input", (e) => this.handleInputChange(e))
|
||||
}
|
||||
|
||||
// Dashboard Component Events
|
||||
const chartViewBtn = document.getElementById("chartViewBtn")
|
||||
const tableViewBtn = document.getElementById("tableViewBtn")
|
||||
const summaryViewBtn = document.getElementById("summaryViewBtn")
|
||||
|
||||
if (chartViewBtn) {
|
||||
chartViewBtn.addEventListener("click", () => this.switchView("chart"))
|
||||
}
|
||||
|
||||
if (tableViewBtn) {
|
||||
tableViewBtn.addEventListener("click", () => this.switchView("table"))
|
||||
}
|
||||
|
||||
if (summaryViewBtn) {
|
||||
summaryViewBtn.addEventListener("click", () => this.switchView("summary"))
|
||||
}
|
||||
|
||||
// Export Buttons
|
||||
const exportExcelBtn = document.getElementById("exportExcelBtn")
|
||||
const exportPDFBtn = document.getElementById("exportPDFBtn")
|
||||
const exportCSVBtn = document.getElementById("exportCSVBtn")
|
||||
const exportJSONBtn = document.getElementById("exportJSONBtn")
|
||||
|
||||
if (exportExcelBtn) {
|
||||
exportExcelBtn.addEventListener("click", () => this.exportData("excel"))
|
||||
}
|
||||
|
||||
if (exportPDFBtn) {
|
||||
exportPDFBtn.addEventListener("click", () => this.exportData("pdf"))
|
||||
}
|
||||
|
||||
if (exportCSVBtn) {
|
||||
exportCSVBtn.addEventListener("click", () => this.exportData("csv"))
|
||||
}
|
||||
|
||||
if (exportJSONBtn) {
|
||||
exportJSONBtn.addEventListener("click", () => this.exportData("json"))
|
||||
}
|
||||
}
|
||||
|
||||
handleDragEnter(e) {
|
||||
e.preventDefault()
|
||||
const uploadArea = document.getElementById("uploadArea")
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.add("border-blue-500", "bg-blue-50")
|
||||
}
|
||||
}
|
||||
|
||||
handleDragLeave(e) {
|
||||
e.preventDefault()
|
||||
const uploadArea = document.getElementById("uploadArea")
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove("border-blue-500", "bg-blue-50")
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(e) {
|
||||
e.preventDefault()
|
||||
const uploadArea = document.getElementById("uploadArea")
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove("border-blue-500", "bg-blue-50")
|
||||
}
|
||||
|
||||
const files = e.dataTransfer.files
|
||||
if (files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
this.handleFileSelect(file)
|
||||
}
|
||||
|
||||
async handleFileSelect(file) {
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
if (errorMessage) {
|
||||
errorMessage.classList.add("hidden")
|
||||
}
|
||||
|
||||
// Validate file
|
||||
const validation = this.fileUploadService.validateFile(file)
|
||||
if (!validation.isValid) {
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = validation.errors.join(", ")
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (file.size > this.fileUploadService.maxFileSize) {
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = `File size exceeds limit of ${this.fileUploadService.maxFileSize / (1024 * 1024)}MB`
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check file format
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase()
|
||||
const acceptedFormats = [".xlsx", ".csv"]
|
||||
if (!acceptedFormats.includes(`.${fileExtension}`)) {
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = `Invalid file format. Accepted formats: ${acceptedFormats.join(", ")}`
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Show selected file info
|
||||
const selectedFileInfo = document.getElementById("selectedFileInfo")
|
||||
const selectedFileName = document.getElementById("selectedFileName")
|
||||
if (selectedFileInfo && selectedFileName) {
|
||||
selectedFileInfo.classList.remove("hidden")
|
||||
selectedFileName.textContent = file.name
|
||||
}
|
||||
|
||||
// Get file preview
|
||||
try {
|
||||
const preview = await this.fileUploadService.getFilePreview(file)
|
||||
this.updateFilePreview(preview, file)
|
||||
} catch (err) {
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = "Failed to preview file"
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateFilePreview(preview, file) {
|
||||
const filePreview = document.getElementById("filePreview")
|
||||
const previewName = document.getElementById("previewName")
|
||||
const previewSize = document.getElementById("previewSize")
|
||||
const previewColumns = document.getElementById("previewColumns")
|
||||
const previewRows = document.getElementById("previewRows")
|
||||
const uploadButton = document.getElementById("uploadButton")
|
||||
|
||||
if (filePreview) filePreview.classList.remove("hidden")
|
||||
if (previewName) previewName.textContent = file.name
|
||||
if (previewSize) previewSize.textContent = Math.round(file.size / 1024)
|
||||
if (previewColumns) previewColumns.textContent = preview.columns.join(", ")
|
||||
if (previewRows) previewRows.textContent = preview.rowCount
|
||||
if (uploadButton) uploadButton.classList.remove("hidden")
|
||||
}
|
||||
|
||||
clearFile() {
|
||||
this.uploadedFile = null
|
||||
this.analysisResults = null
|
||||
|
||||
// Hide components
|
||||
const uploadComponent = document.getElementById("uploadComponent")
|
||||
const intentInputComponent = document.getElementById("intentInputComponent")
|
||||
const dashboardComponent = document.getElementById("dashboardComponent")
|
||||
|
||||
if (uploadComponent) uploadComponent.classList.remove("hidden")
|
||||
if (intentInputComponent) intentInputComponent.classList.add("hidden")
|
||||
if (dashboardComponent) dashboardComponent.classList.add("hidden")
|
||||
|
||||
// Reset upload area
|
||||
const uploadArea = document.getElementById("uploadArea")
|
||||
if (uploadArea) {
|
||||
uploadArea.classList.remove("border-blue-500", "bg-blue-50")
|
||||
}
|
||||
|
||||
// Clear file preview and selected file info
|
||||
const filePreview = document.getElementById("filePreview")
|
||||
const selectedFileInfo = document.getElementById("selectedFileInfo")
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
const uploadButton = document.getElementById("uploadButton")
|
||||
|
||||
if (filePreview) filePreview.classList.add("hidden")
|
||||
if (selectedFileInfo) selectedFileInfo.classList.add("hidden")
|
||||
if (errorMessage) errorMessage.classList.add("hidden")
|
||||
if (uploadButton) uploadButton.classList.add("hidden")
|
||||
}
|
||||
|
||||
async uploadFile() {
|
||||
const selectedFileInfo = document.getElementById("selectedFileInfo")
|
||||
const selectedFileName = document.getElementById("selectedFileName")
|
||||
|
||||
if (!selectedFileInfo || !selectedFileName) return
|
||||
|
||||
const fileName = selectedFileName.textContent
|
||||
const fileInput = document.getElementById("fileInput")
|
||||
const file = fileInput?.files?.[0]
|
||||
|
||||
if (!file) {
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = "No file selected"
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Show upload progress
|
||||
const uploadFileBtn = document.getElementById("uploadFileBtn")
|
||||
if (uploadFileBtn) {
|
||||
uploadFileBtn.textContent = "Uploading..."
|
||||
uploadFileBtn.disabled = true
|
||||
}
|
||||
|
||||
// Upload file
|
||||
this.uploadedFile = await this.fileUploadService.uploadFile(file)
|
||||
|
||||
// Show next component
|
||||
const intentInputComponent = document.getElementById("intentInputComponent")
|
||||
if (intentInputComponent) {
|
||||
intentInputComponent.classList.remove("hidden")
|
||||
}
|
||||
|
||||
// Hide upload component
|
||||
if (uploadFileBtn) {
|
||||
uploadFileBtn.textContent = "Upload File"
|
||||
uploadFileBtn.disabled = false
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = "Failed to upload file"
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange(e) {
|
||||
const userInput = e.target.value
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
const recognizedIntent = document.getElementById("recognizedIntent")
|
||||
const suggestions = document.getElementById("suggestions")
|
||||
|
||||
if (errorMessage) errorMessage.classList.add("hidden")
|
||||
if (recognizedIntent) recognizedIntent.classList.add("hidden")
|
||||
if (suggestions) suggestions.classList.add("hidden")
|
||||
}
|
||||
|
||||
async recognizeIntent() {
|
||||
const userInput = document.getElementById("userInput")
|
||||
const analyzeBtn = document.getElementById("analyzeBtn")
|
||||
const processingIndicator = document.getElementById("processingIndicator")
|
||||
|
||||
if (!userInput || !analyzeBtn || !processingIndicator) return
|
||||
|
||||
const input = userInput.value.trim()
|
||||
if (!input) {
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = "Please enter your analysis request"
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Show processing indicator
|
||||
if (processingIndicator) processingIndicator.classList.remove("hidden")
|
||||
if (analyzeBtn) {
|
||||
analyzeBtn.textContent = "Analyzing..."
|
||||
analyzeBtn.disabled = true
|
||||
}
|
||||
|
||||
try {
|
||||
const intent = await this.aiService.recognizeIntent(input)
|
||||
const parameters = await this.aiService.extractParameters(intent)
|
||||
|
||||
this.analysisResults = await this.aiService.analyzeData(this.uploadedFile.data, intent)
|
||||
|
||||
// Show recognized intent
|
||||
const intentType = document.getElementById("intentType")
|
||||
const intentContext = document.getElementById("intentContext")
|
||||
const intentParameters = document.getElementById("intentParameters")
|
||||
const recognizedIntent = document.getElementById("recognizedIntent")
|
||||
|
||||
if (intentType) intentType.textContent = intent.type
|
||||
if (intentContext) intentContext.textContent = intent.context
|
||||
if (intentParameters) intentParameters.textContent = JSON.stringify(intent.parameters, null, 2)
|
||||
if (recognizedIntent) recognizedIntent.classList.remove("hidden")
|
||||
|
||||
// Show suggestions
|
||||
const suggestionsList = document.getElementById("suggestionsList")
|
||||
const suggestions = await this.aiService.getSimilarRequests(intent)
|
||||
|
||||
if (suggestionsList) {
|
||||
suggestionsList.innerHTML = suggestions
|
||||
.map(
|
||||
(suggestion) =>
|
||||
`<button class="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors"
|
||||
onclick="excelAnalyzer.handleSuggestionClick('${suggestion.replace(/'/g, "\\'")}')">${suggestion}</button>`,
|
||||
)
|
||||
.join("")
|
||||
const suggestionsContainer = document.getElementById("suggestions")
|
||||
if (suggestionsContainer) suggestionsContainer.classList.remove("hidden")
|
||||
}
|
||||
|
||||
// Show dashboard component
|
||||
const dashboardComponent = document.getElementById("dashboardComponent")
|
||||
if (dashboardComponent) {
|
||||
dashboardComponent.classList.remove("hidden")
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = "Failed to recognize intent. Please try again."
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
} finally {
|
||||
if (processingIndicator) processingIndicator.classList.add("hidden")
|
||||
if (analyzeBtn) {
|
||||
analyzeBtn.textContent = "Analyze"
|
||||
analyzeBtn.disabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSuggestionClick(suggestion) {
|
||||
const userInput = document.getElementById("userInput")
|
||||
const suggestionsList = document.getElementById("suggestionsList")
|
||||
|
||||
if (userInput) {
|
||||
userInput.value = suggestion
|
||||
}
|
||||
|
||||
if (suggestionsList) {
|
||||
suggestionsList.innerHTML = ""
|
||||
const suggestions = document.getElementById("suggestions")
|
||||
if (suggestions) suggestions.classList.add("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
clearIntent() {
|
||||
const userInput = document.getElementById("userInput")
|
||||
const recognizedIntent = document.getElementById("recognizedIntent")
|
||||
const suggestions = document.getElementById("suggestions")
|
||||
const processingIndicator = document.getElementById("processingIndicator")
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
|
||||
if (userInput) userInput.value = ""
|
||||
if (recognizedIntent) recognizedIntent.classList.add("hidden")
|
||||
if (suggestions) suggestions.classList.add("hidden")
|
||||
if (processingIndicator) processingIndicator.classList.add("hidden")
|
||||
if (errorMessage) errorMessage.classList.add("hidden")
|
||||
}
|
||||
|
||||
switchView(view) {
|
||||
const chartViewBtn = document.getElementById("chartViewBtn")
|
||||
const tableViewBtn = document.getElementById("tableViewBtn")
|
||||
const summaryViewBtn = document.getElementById("summaryViewBtn")
|
||||
|
||||
// Update button styles
|
||||
if (chartViewBtn) {
|
||||
chartViewBtn.className = `px-4 py-2 rounded-lg transition-colors ${view === "chart" ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-700 hover:bg-gray-300"}`
|
||||
}
|
||||
if (tableViewBtn) {
|
||||
tableViewBtn.className = `px-4 py-2 rounded-lg transition-colors ${view === "table" ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-700 hover:bg-gray-300"}`
|
||||
}
|
||||
if (summaryViewBtn) {
|
||||
summaryViewBtn.className = `px-4 py-2 rounded-lg transition-colors ${view === "summary" ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-700 hover:bg-gray-300"}`
|
||||
}
|
||||
|
||||
this.currentView = view
|
||||
this.renderView()
|
||||
}
|
||||
|
||||
renderView() {
|
||||
const mainContent = document.getElementById("mainContent")
|
||||
|
||||
if (!mainContent || !this.analysisResults) return
|
||||
|
||||
let viewContent
|
||||
|
||||
switch (this.currentView) {
|
||||
case "chart":
|
||||
viewContent = this.renderChart()
|
||||
break
|
||||
case "table":
|
||||
viewContent = this.renderTable()
|
||||
break
|
||||
case "summary":
|
||||
viewContent = this.renderSummary()
|
||||
break
|
||||
default:
|
||||
viewContent =
|
||||
'<div class="text-center py-8 text-gray-500"><p>No analysis results available. Please upload a file and perform analysis.</p></div>'
|
||||
}
|
||||
|
||||
mainContent.innerHTML = viewContent
|
||||
}
|
||||
|
||||
renderChart() {
|
||||
if (!this.analysisResults || !this.analysisResults.charts || this.analysisResults.charts.length === 0) {
|
||||
return '<div class="text-center py-8 text-gray-500"><p>No chart data available</p></div>'
|
||||
}
|
||||
|
||||
const chartData = this.analysisResults.charts[0]
|
||||
const chartId = "analysisChart"
|
||||
|
||||
// Create canvas element
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.id = chartId
|
||||
canvas.className = "relative h-64"
|
||||
|
||||
// Append to main content
|
||||
const mainContent = document.getElementById("mainContent")
|
||||
mainContent.innerHTML = ""
|
||||
mainContent.appendChild(canvas)
|
||||
|
||||
// Create chart
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (ctx) {
|
||||
const chartConfig = {
|
||||
type: chartData.type,
|
||||
data: {
|
||||
labels: chartData.data.map((row) => row[0]),
|
||||
datasets: [
|
||||
{
|
||||
label: chartData.title,
|
||||
data: chartData.data.map((row) => row[1]),
|
||||
backgroundColor: "rgba(54, 162, 235, 0.5)",
|
||||
borderColor: "rgba(54, 162, 235, 1)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return new Chart(ctx, chartConfig)
|
||||
}
|
||||
|
||||
return '<div class="text-center py-8 text-gray-500"><p>Failed to render chart</p></div>'
|
||||
}
|
||||
|
||||
renderTable() {
|
||||
if (!this.analysisResults || !this.analysisResults.tables || this.analysisResults.tables.length === 0) {
|
||||
return '<div class="text-center py-8 text-gray-500"><p>No table data available</p></div>'
|
||||
}
|
||||
|
||||
const tables = this.analysisResults.tables
|
||||
let tableContent = ""
|
||||
|
||||
tables.forEach((table, index) => {
|
||||
tableContent += `
|
||||
<div class="mb-4">
|
||||
<h4 class="font-medium mb-2">${table.title}</h4>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b">
|
||||
${table.columns
|
||||
.map((column) => `<th class="px-4 py-2 text-left">${column}</th>`)
|
||||
.join("")}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${table.data
|
||||
.map(
|
||||
(row, rowIndex) =>
|
||||
`<tr class="border-b hover:bg-gray-50">
|
||||
${row.map((cell, cellIndex) => `<td class="px-4 py-2">${cell}</td>`).join("")}
|
||||
</tr>`,
|
||||
)
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
|
||||
return `
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold mb-3">Data Table</h3>
|
||||
${tableContent || '<p class="text-gray-500">No table data available</p>'}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
renderSummary() {
|
||||
if (!this.analysisResults || !this.analysisResults.summary) {
|
||||
return '<div class="text-center py-8 text-gray-500"><p>No summary data available</p></div>'
|
||||
}
|
||||
|
||||
const summary = this.analysisResults.summary
|
||||
|
||||
return `
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold mb-3">Analysis Summary</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="bg-blue-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium mb-2">Key Metrics</h4>
|
||||
<div class="space-y-2">
|
||||
${Object.entries(summary.metrics || {})
|
||||
.map(
|
||||
([metric, value]) =>
|
||||
`<div class="flex justify-between">
|
||||
<span class="text-gray-600">${metric}:</span>
|
||||
<span class="font-medium">${value}</span>
|
||||
</div>`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium mb-2">Insights</h4>
|
||||
${
|
||||
summary.insights && summary.insights.length > 0
|
||||
? `
|
||||
${summary.insights
|
||||
.map(
|
||||
(insight, index) =>
|
||||
`<div class="mb-2 p-2 bg-white rounded">
|
||||
<p class="text-sm font-medium text-green-800">${insight.description}</p>
|
||||
<p class="text-xs text-gray-600">Severity: ${insight.severity}</p>
|
||||
</div>`,
|
||||
)
|
||||
.join("")}
|
||||
`
|
||||
: '<p class="text-gray-500">No insights available</p>'
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="bg-purple-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium mb-2">Recommendations</h4>
|
||||
${
|
||||
summary.recommendations && summary.recommendations.length > 0
|
||||
? `
|
||||
${summary.recommendations
|
||||
.map(
|
||||
(recommendation, index) =>
|
||||
`<div class="mb-2 p-2 bg-white rounded">
|
||||
<p class="text-sm font-medium text-purple-800">${recommendation.description}</p>
|
||||
<p class="text-xs text-gray-600">Priority: ${recommendation.priority}</p>
|
||||
<ul class="text-xs text-gray-600 mt-1">
|
||||
${recommendation.steps.map((step, stepIndex) => `<li>- ${step}</li>`).join("")}
|
||||
</ul>
|
||||
</div>`,
|
||||
)
|
||||
.join("")}
|
||||
`
|
||||
: '<p class="text-gray-500">No recommendations available</p>'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
exportData(format) {
|
||||
if (!this.analysisResults) {
|
||||
const errorMessage = document.getElementById("errorMessage")
|
||||
if (errorMessage) {
|
||||
errorMessage.textContent = "No analysis results to export"
|
||||
errorMessage.classList.remove("hidden")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Mock export functionality
|
||||
let data = JSON.stringify(this.analysisResults, null, 2)
|
||||
let filename = "analysis-results"
|
||||
let contentType = "application/json"
|
||||
|
||||
switch (format) {
|
||||
case "excel":
|
||||
filename += ".xlsx"
|
||||
// Note: Actual Excel export would require additional libraries
|
||||
break
|
||||
case "pdf":
|
||||
filename += ".pdf"
|
||||
// Note: Actual PDF export would require additional libraries
|
||||
break
|
||||
case "csv":
|
||||
filename += ".csv"
|
||||
// Convert to CSV format
|
||||
data = this.convertToCSV(this.analysisResults)
|
||||
contentType = "text/csv"
|
||||
break
|
||||
case "json":
|
||||
filename += ".json"
|
||||
break
|
||||
}
|
||||
|
||||
// Create and download file
|
||||
const blob = new Blob([data], { type: contentType })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
convertToCSV(data) {
|
||||
// Simple CSV conversion for demonstration
|
||||
const results = data
|
||||
const rows = []
|
||||
|
||||
// Add headers
|
||||
const headers = ["Type", "Metrics", "Trends", "Patterns", "Anomalies", "Insights", "Recommendations"]
|
||||
rows.push(headers.join(","))
|
||||
|
||||
// Add data
|
||||
const row = [
|
||||
"Summary",
|
||||
JSON.stringify(results.summary?.metrics || ""),
|
||||
JSON.stringify(results.summary?.trends || ""),
|
||||
JSON.stringify(results.summary?.patterns || ""),
|
||||
JSON.stringify(results.summary?.anomalies || ""),
|
||||
JSON.stringify(results.summary?.insights || ""),
|
||||
JSON.stringify(results.summary?.recommendations || ""),
|
||||
]
|
||||
rows.push(row.join(","))
|
||||
|
||||
return rows.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the Excel Analyzer
|
||||
const excelAnalyzer = new ExcelAnalyzer()
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
e# Excel Analyzer Architecture (Revised)
|
||||
|
||||
## 1) Architecture Style
|
||||
|
||||
The project uses a **client-side modular SPA-style architecture**:
|
||||
|
||||
- Runs fully in the browser (no backend required)
|
||||
- Processes files locally for privacy
|
||||
- Uses module separation for upload, processing, visualization, dashboard, export
|
||||
- Supports extensibility through a dedicated **Hook Engine**
|
||||
|
||||
## 2) High-Level Layers
|
||||
|
||||
1. **Presentation Layer**
|
||||
- `src/index.html`
|
||||
- `src/css/styles.css`
|
||||
|
||||
2. **Application Orchestration Layer**
|
||||
- `src/js/main.js`
|
||||
- Coordinates upload → preview → analyze → dashboard → export flow
|
||||
|
||||
3. **Domain Modules Layer**
|
||||
- `src/js/modules/*.js`
|
||||
- Encapsulates feature logic (file upload, data processing, charts, dashboard, exports)
|
||||
|
||||
4. **Shared Utilities and Config Layer**
|
||||
- `src/js/utils/*.js`
|
||||
- `src/js/config/*.js`
|
||||
- `src/js/events/*.js`
|
||||
|
||||
5. **Extensibility Layer (Hooks)**
|
||||
- `src/hooks/hook-engine.js`
|
||||
- Central hook registry/execution engine for plugging cross-cutting behavior
|
||||
|
||||
## 3) Hook Engine Design
|
||||
|
||||
Location: `src/hooks/hook-engine.js`
|
||||
|
||||
Core capabilities:
|
||||
|
||||
- `tap(hookName, handler, { priority, once })`
|
||||
- `tapOnce(hookName, handler, options)`
|
||||
- `untap(hookName, hookId)`
|
||||
- `clear(hookName?)`
|
||||
- `has(hookName)` / `list(hookName)`
|
||||
- `run(hookName, context)` for async handler execution
|
||||
- `runWaterfall(hookName, payload, context)` for payload transformation pipeline
|
||||
|
||||
Design goals:
|
||||
|
||||
- Keep hook logic isolated in `src/hooks/`
|
||||
- Make extension points explicit and reusable
|
||||
- Allow priority ordering and one-time hooks
|
||||
|
||||
## 4) Data Flow Layer
|
||||
|
||||
```text
|
||||
User File Input
|
||||
-> FileUploadManager
|
||||
-> DataProcessor
|
||||
-> Visualizer / DashboardManager
|
||||
-> Exporter
|
||||
-> Download Output
|
||||
```
|
||||
|
||||
Potential hook interception points (recommended):
|
||||
|
||||
- `beforeFileParse`
|
||||
- `afterFileParse`
|
||||
- `beforeAnalysis`
|
||||
- `afterAnalysis`
|
||||
- `beforeExport`
|
||||
- `afterExport`
|
||||
|
||||
## 5) Documentation and Operational Memory
|
||||
|
||||
Beyond source code, the project now includes lightweight operational artifacts:
|
||||
|
||||
- `CLAUDE.md` — shared brain / durable lessons learned
|
||||
- `.orchestration/active_intents.yaml` — active priorities/intents
|
||||
- `.orchestration/agent_trace.jsonl` — chronological action trace
|
||||
- `.orchestration/intent_map.md` — mapping goals to code areas
|
||||
|
||||
## 6) Architectural Principles
|
||||
|
||||
- **Separation of concerns** between UI, orchestration, domain logic, utilities, and hooks
|
||||
- **Local-first processing** for privacy and performance
|
||||
- **Incremental extensibility** via Hook Engine rather than hardcoded branching
|
||||
- **Documentation alignment**: structure docs must reflect actual repository state
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
# Excel Analyzer Project Constitution
|
||||
|
||||
## Preamble
|
||||
|
||||
We, the developers and contributors to the Excel Analyzer Project, recognize the importance of creating a robust, user-friendly, and accessible web-based tool for Excel data analysis. This constitution establishes the fundamental principles, values, and governance the structure that guide our development efforts and ensure the project's success and sustainability.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. User-Centric Design
|
||||
- **1.1** The user experience SHALL be the primary consideration in all design decisions
|
||||
- **1.2** The application SHALL be intuitive and accessible to users of all technical backgrounds
|
||||
- **1.3** User feedback SHALL be actively sought and incorporated into development cycles
|
||||
|
||||
### 2. Data Privacy and Security
|
||||
- **2.1** User data SHALL remain on the client-side and SHALL NOT be transmitted to external servers
|
||||
- **2.2** No persistent storage of user files SHALL occur without explicit user consent
|
||||
- **2.3** All data processing SHALL be transparent and auditable by the user
|
||||
|
||||
### 3. Open Source and Collaboration
|
||||
- **3.1** The project SHALL remain open source under the MIT License
|
||||
- **3.2** Contributions from the community SHALL be welcomed and encouraged
|
||||
- **3.3** Development processes SHALL be transparent and documented
|
||||
|
||||
### 4. Technical Excellence
|
||||
- **4.1** Code quality SHALL meet industry standards and best practices
|
||||
- **4.2** Performance optimization SHALL be prioritized for user experience
|
||||
- **4.3** Cross-browser compatibility SHALL be maintained and tested
|
||||
|
||||
### 5. Accessibility and Inclusion
|
||||
- **5.1** The application SHALL comply with WCAG 2.1 accessibility guidelines
|
||||
- **5.2** Support for assistive technologies SHALL be maintained
|
||||
- **5.3** Internationalization SHALL be considered in design and implementation
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### 6. Code Standards
|
||||
- **6.1** All code SHALL be well-documented with clear comments and documentation
|
||||
- **6.2** Consistent coding style SHALL be enforced using automated tools
|
||||
- **6.3** Code reviews SHALL be mandatory for all contributions
|
||||
- **6.4** Unit tests SHALL be written for all new functionality
|
||||
|
||||
### 7. Quality Assurance
|
||||
- **7.1** All features SHALL be thoroughly tested before release
|
||||
- **7.2** Performance benchmarks SHALL be established and monitored
|
||||
- **7.3** Security vulnerabilities SHALL be addressed promptly
|
||||
- **7.4** User acceptance testing SHALL be conducted for major features
|
||||
|
||||
### 8. Version Control and Releases
|
||||
- **8.1** Git SHALL be used for version control with a clear branching strategy
|
||||
- **8.2** Semantic versioning SHALL be followed for releases
|
||||
- **8.3** Release notes SHALL document all changes and improvements
|
||||
- **8.4** Backward compatibility SHALL be maintained whenever possible
|
||||
|
||||
## Project Governance
|
||||
|
||||
### 9. Decision Making
|
||||
- **9.1** Technical decisions SHALL be made through consensus among core contributors
|
||||
- **9.2** Major architectural changes SHALL require community discussion
|
||||
- **9.3** User-facing changes SHALL be validated through user testing
|
||||
- **9.4** Emergency fixes MAY be implemented immediately with post-facto documentation
|
||||
|
||||
### 10. Contribution Process
|
||||
- **10.1** All contributors SHALL adhere to the project's code of conduct
|
||||
- **10.2** Pull requests SHALL include appropriate tests and documentation
|
||||
- **10.3** Contributors SHALL be respectful and constructive in all interactions
|
||||
- **10.4** Mentorship of new contributors SHALL be encouraged
|
||||
|
||||
### 11. Maintenance and Support
|
||||
- **11.1** Regular maintenance updates SHALL be scheduled and published
|
||||
- **11.2** Bug reports SHALL be acknowledged and addressed in a timely manner
|
||||
- **11.3** Security issues SHALL be handled with appropriate urgency
|
||||
- **11.4** Legacy support SHALL be maintained for reasonable time periods
|
||||
|
||||
## Ethical Guidelines
|
||||
|
||||
### 12. Data Ethics
|
||||
- **12.1** The application SHALL NOT make assumptions or inferences about user data
|
||||
- **12.2** Data analysis features SHALL be transparent about their methodologies
|
||||
- **12.3** Users SHALL retain full control over their data and analysis results
|
||||
- **12.4** The application SHALL NOT collect or analyze user behavior without consent
|
||||
|
||||
### 13. Responsible Development
|
||||
- **13.1** Environmental impact of the application SHALL be considered
|
||||
- **13.2** Resource efficiency SHALL be optimized to reduce computational waste
|
||||
- **13.3** Dependencies SHALL be chosen with security and maintenance in mind
|
||||
- **13.4** The project SHALL contribute positively to the open-source ecosystem
|
||||
|
||||
## Amendments
|
||||
|
||||
### 14. Constitution Updates
|
||||
- **14.1** This constitution MAY be amended through community consensus
|
||||
- **14.2** Proposed amendments SHALL be discussed publicly for a minimum of 30 days
|
||||
- **14.3** Amendments SHALL require approval from the core development team
|
||||
- **14.4** All amendments SHALL be documented with rationale and impact assessment
|
||||
|
||||
## Commitment
|
||||
|
||||
By contributing to this project, all participants agree to uphold these principles and work towards creating an Excel analysis tool that is not only technically excellent but also ethically sound, user-focused, and accessible to all.
|
||||
|
||||
**Adopted**: February 17, 2026
|
||||
**Version**: 1.0
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
# Excel Analyzer Project Implementation Plan
|
||||
|
||||
## Project Overview
|
||||
|
||||
This document outlines the comprehensive implementation plan for the web-based Excel Analyzer project, detailing the development phases, milestones, and resource allocation required to deliver a complete solution.
|
||||
|
||||
## Project Phases
|
||||
|
||||
### Phase 1: Foundation Setup (Week 1)
|
||||
**Duration**: 2 days
|
||||
**Objective**: Establish project structure and basic infrastructure
|
||||
|
||||
#### 1.1 Project Initialization
|
||||
- [ ] Create project directory structure
|
||||
- [ ] Set up HTML, CSS, and JavaScript file organization
|
||||
- [ ] Configure development environment
|
||||
- [ ] Initialize documentation files
|
||||
|
||||
#### 1.2 Core Dependencies Setup
|
||||
- [ ] Integrate SheetJS (xlsx.js) for Excel file processing
|
||||
- [ ] Add Chart.js for data visualization
|
||||
- [ ] Include jsPDF for PDF export functionality
|
||||
- [ ] Set up CSS framework (Bootstrap or custom styles)
|
||||
|
||||
#### 1.3 Basic File Upload Interface
|
||||
- [ ] Create main HTML structure
|
||||
- [ ] Implement file input and drag-and-drop functionality
|
||||
- [ ] Add file validation and error handling
|
||||
- [ ] Create basic progress indicators
|
||||
|
||||
**Deliverable**: Basic file upload and validation system
|
||||
|
||||
### Phase 2: Data Processing Engine (Week 2)
|
||||
**Duration**: 1 days
|
||||
**Objective**: Implement core data analysis and preprocessing capabilities
|
||||
|
||||
#### 2.1 Data Parsing and Analysis
|
||||
- [ ] Implement Excel file parsing using SheetJS
|
||||
- [ ] Create data structure for internal data representation
|
||||
- [ ] Develop data type detection algorithms
|
||||
- [ ] Implement basic data statistics calculation
|
||||
|
||||
#### 2.2 Data Preprocessing Module
|
||||
- [ ] Missing value detection and handling
|
||||
- [ ] Outlier identification algorithms
|
||||
- [ ] Data cleaning strategies implementation
|
||||
- [ ] Data validation and quality checks
|
||||
|
||||
#### 2.3 Data Observation Interface
|
||||
- [ ] Create data preview table component
|
||||
- [ ] Implement data summary display
|
||||
- [ ] Add data quality indicators
|
||||
- [ ] Create interactive data exploration features
|
||||
|
||||
**Deliverable**: Complete data processing and observation system
|
||||
|
||||
### Phase 3: Visualization System (Week 3)
|
||||
**Duration**: 1-2 days
|
||||
**Objective**: Develop comprehensive data visualization capabilities
|
||||
|
||||
#### 3.1 Chart Generation Engine
|
||||
- [ ] Implement automatic chart type selection based on data
|
||||
- [ ] Create bar chart visualization
|
||||
- [ ] Create line chart visualization
|
||||
- [ ] Create pie chart visualization
|
||||
- [ ] Create scatter plot visualization
|
||||
|
||||
#### 3.2 Interactive Visualization Features
|
||||
- [ ] Add chart customization options
|
||||
- [ ] Implement zoom and pan functionality
|
||||
- [ ] Add tooltips and data point highlighting
|
||||
- [ ] Create real-time chart updates
|
||||
|
||||
#### 3.3 Visualization Interface
|
||||
- [ ] Create chart selection and configuration panel
|
||||
- [ ] Implement chart arrangement and layout options
|
||||
- [ ] Add chart export functionality
|
||||
- [ ] Create visualization history and presets
|
||||
|
||||
**Deliverable**: Complete interactive visualization system
|
||||
|
||||
### Phase 4: Dashboard and User Interface (Week 4)
|
||||
**Duration**: 1 day
|
||||
**Objective**: Build comprehensive dashboard and enhance user experience
|
||||
|
||||
#### 4.1 Dashboard Framework
|
||||
- [ ] Create dashboard layout system
|
||||
- [ ] Implement widget-based dashboard components
|
||||
- [ ] Add dashboard customization and arrangement
|
||||
- [ ] Create dashboard state management
|
||||
|
||||
#### 4.2 Advanced User Interface
|
||||
- [ ] Implement responsive design for mobile devices
|
||||
- [ ] Add accessibility features (WCAG compliance)
|
||||
- [ ] Create user preferences and settings
|
||||
- [ ] Implement keyboard navigation
|
||||
|
||||
#### 4.3 Data Filtering and Interaction
|
||||
- [ ] Add real-time data filtering capabilities
|
||||
- [ ] Implement cross-chart data linking
|
||||
- [ ] Create data drill-down features
|
||||
- [ ] Add dashboard export and sharing options
|
||||
|
||||
**Deliverable**: Complete interactive dashboard system
|
||||
|
||||
### Phase 5: Export and Advanced Features (Week 5)
|
||||
**Duration**: 1 day
|
||||
**Objective**: Implement export functionality and advanced features
|
||||
|
||||
#### 5.1 Export Module
|
||||
- [ ] Implement PDF export with charts and analysis
|
||||
- [ ] Create Excel export for processed data
|
||||
- [ ] Add export progress indicators
|
||||
- [ ] Implement batch export functionality
|
||||
|
||||
#### 5.2 Advanced Analytics
|
||||
- [ ] Add statistical analysis features
|
||||
- [ ] Implement trend analysis
|
||||
- [ ] Create data comparison tools
|
||||
- [ ] Add custom calculation support
|
||||
|
||||
#### 5.3 Performance Optimization
|
||||
- [ ] Optimize file processing for large datasets
|
||||
- [ ] Implement lazy loading for charts
|
||||
- [ ] Add memory management for large files
|
||||
- [ ] Optimize export generation speed
|
||||
|
||||
**Deliverable**: Complete export system and advanced analytics
|
||||
|
||||
### Phase 6: Testing and Documentation (Week 6)
|
||||
**Duration**: 1 days
|
||||
**Objective**: Ensure quality and create comprehensive documentation
|
||||
|
||||
#### 6.1 Testing and Quality Assurance
|
||||
- [ ] Create comprehensive test suite
|
||||
- [ ] Perform cross-browser compatibility testing
|
||||
- [ ] Conduct performance testing
|
||||
- [ ] Implement user acceptance testing
|
||||
|
||||
#### 6.2 Documentation and Help System
|
||||
- [ ] Create user guide and documentation
|
||||
- [ ] Implement in-app help system
|
||||
- [ ] Create API documentation
|
||||
- [ ] Add tooltips and contextual help
|
||||
|
||||
#### 6.3 Final Polish and Optimization
|
||||
- [ ] Address all identified bugs and issues
|
||||
- [ ] Optimize code for production
|
||||
- [ ] Create deployment package
|
||||
- [ ] Final performance tuning
|
||||
|
||||
**Deliverable**: Production-ready application with complete documentation
|
||||
|
||||
## Resource Allocation
|
||||
|
||||
### Development Team
|
||||
- **Project Manager**: 1 person (part-time)
|
||||
- **Frontend Developer**: 1 person (full-time)
|
||||
- **UI/UX Designer**: 1 person (part-time)
|
||||
- **Quality Assurance**: 1 person (part-time)
|
||||
|
||||
### Technology Resources
|
||||
- **Development Environment**: Modern web browser, code editor
|
||||
- **Libraries**: SheetJS, Chart.js, jsPDF, Bootstrap
|
||||
- **Testing Tools**: Browser developer tools, manual testing
|
||||
- **Documentation**: Markdown, HTML documentation
|
||||
|
||||
### Time Allocation
|
||||
- **Total Duration**: 1 weeks
|
||||
- **Development Time**: 60-70 hours
|
||||
- **Testing Time**: 10-12 hours
|
||||
- **Documentation Time**: 2-3 hours
|
||||
|
||||
## Risk Management
|
||||
|
||||
### Technical Risks
|
||||
- **Large File Processing**: Mitigation through chunking and optimization
|
||||
- **Browser Compatibility**: Mitigation through comprehensive testing
|
||||
- **Performance Issues**: Mitigation through optimization and best practices
|
||||
|
||||
### Project Risks
|
||||
- **Scope Creep**: Mitigation through clear requirements and phase boundaries
|
||||
- **Resource Constraints**: Mitigation through careful planning and prioritization
|
||||
- **Timeline Delays**: Mitigation through buffer time and parallel development
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Functional Success
|
||||
- [ ] All specified features implemented and working
|
||||
- [ ] File processing handles up to 10,000 rows efficiently
|
||||
- [ ] Export functionality produces valid files
|
||||
- [ ] Dashboard provides interactive data exploration
|
||||
|
||||
### Quality Success
|
||||
- [ ] Application works across all major browsers
|
||||
- [ ] Performance meets specified benchmarks
|
||||
- [ ] Code quality meets established standards
|
||||
- [ ] User interface is intuitive and accessible
|
||||
|
||||
### User Success
|
||||
- [ ] Users can complete analysis tasks within 15 minutes
|
||||
- [ ] Application receives positive user feedback
|
||||
- [ ] Documentation enables self-service usage
|
||||
- [ ] Application meets accessibility standards
|
||||
|
||||
## Post-Implementation
|
||||
|
||||
### Maintenance Plan
|
||||
- Regular security updates for dependencies
|
||||
- Performance monitoring and optimization
|
||||
- User feedback collection and implementation
|
||||
- Bug fixes and minor feature enhancements
|
||||
|
||||
### Future Enhancements
|
||||
- Cloud storage integration
|
||||
- Advanced machine learning features
|
||||
- Collaboration and sharing capabilities
|
||||
- Mobile app development
|
||||
|
||||
This implementation plan provides a structured approach to developing the Excel Analyzer project, ensuring all requirements are met while maintaining quality and user satisfaction.
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# Excel Analyzer — Project Structure (Revised)
|
||||
|
||||
This document reflects the current repository layout after adding:
|
||||
|
||||
- `src/hooks/hook-engine.js`
|
||||
- `.orchestration/*`
|
||||
- `CLAUDE.md`
|
||||
|
||||
## Repository Tree
|
||||
|
||||
```text
|
||||
Week1_TRP1_Intent_based_planning/
|
||||
├── README.md
|
||||
├── specify/
|
||||
│ └── specification.md
|
||||
└── Excel-Based-Data-Analzer/
|
||||
├── README.md
|
||||
├── CLAUDE.md
|
||||
├── .orchestration/
|
||||
│ ├── active_intents.yaml
|
||||
│ ├── agent_trace.jsonl
|
||||
│ └── intent_map.md
|
||||
├── specify/
|
||||
│ ├── specification.md
|
||||
│ ├── memory/
|
||||
│ │ └── constitution.md
|
||||
│ ├── plan/
|
||||
│ │ └── plan.md
|
||||
│ ├── architecture/
|
||||
│ │ └── architecture.md
|
||||
│ └── project_structure/
|
||||
│ └── project_structure.md
|
||||
└── src/
|
||||
├── index.html
|
||||
├── css/
|
||||
│ └── styles.css
|
||||
├── hooks/
|
||||
│ └── hook-engine.js
|
||||
└── js/
|
||||
├── main.js
|
||||
├── config/
|
||||
│ ├── defaults.js
|
||||
│ ├── settings.js
|
||||
│ └── themes.js
|
||||
├── events/
|
||||
│ ├── event-handlers.js
|
||||
│ └── event-system.js
|
||||
├── modules/
|
||||
│ ├── analytics.js
|
||||
│ ├── dashboard.js
|
||||
│ ├── data-processor.js
|
||||
│ ├── exporter.js
|
||||
│ ├── file-upload.js
|
||||
│ ├── help.js
|
||||
│ ├── notifications.js
|
||||
│ ├── settings.js
|
||||
│ └── visualizer.js
|
||||
└── utils/
|
||||
├── formatters.js
|
||||
├── helpers.js
|
||||
└── validators.js
|
||||
```
|
||||
|
||||
## Structure Notes
|
||||
|
||||
- `src/hooks/` is reserved for hook-system code (currently `hook-engine.js`).
|
||||
- `.orchestration/` stores operational memory and tracing artifacts.
|
||||
- `CLAUDE.md` stores durable lessons and working agreements.
|
||||
- `specify/` contains project governance/planning/specification documents.
|
||||
|
||||
## Maintenance Rule
|
||||
|
||||
Whenever files/folders are reorganized, update this
|
||||
document and both README files in the same change.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# Excel Analyzer Project Specification
|
||||
|
||||
## 1. Functional Requirements
|
||||
|
||||
### 1.1 File Upload Module
|
||||
- **FR-001**: System SHALL accept Excel files (.xlsx, .xls .csv) from users
|
||||
- **FR-002**: System SHALL validate file format and size (max 50MB)
|
||||
- **FR-003**: System SHALL display file upload progress
|
||||
- **FR-004**: System SHALL provide error messages for invalid files
|
||||
- **FR-005**: System SHALL support drag-and-drop file upload
|
||||
|
||||
### 1.2 Data Observation Module
|
||||
- **FR-006**: System SHALL display uploaded data in a tabular format
|
||||
- **FR-007**: System SHALL show basic data statistics (rows, columns, data types)
|
||||
- **FR-008**: System SHALL allow users to preview first 100 rows
|
||||
- **FR-009**: System SHALL highlight missing values and data anomalies
|
||||
- **FR-010**: System SHALL provide data summary information
|
||||
|
||||
### 1.3 Data Preprocessing Module
|
||||
- **FR-011**: System SHALL detect and handle missing values
|
||||
- **FR-012**: System SHALL identify and flag outliers
|
||||
- **FR-013**: System SHALL provide options for data cleaning strategies
|
||||
- **FR-014**: System SHALL allow users to apply preprocessing rules
|
||||
- **FR-015**: System SHALL maintain data integrity during preprocessing
|
||||
|
||||
### 1.4 Visualization Module
|
||||
- **FR-016**: System SHALL generate automatic charts based on data types
|
||||
- **FR-017**: System SHALL support multiple chart types (bar, line, pie, scatter)
|
||||
- **FR-018**: System SHALL allow users to customize chart appearance
|
||||
- **FR-019**: System SHALL provide interactive chart features (zoom, pan, tooltips)
|
||||
- **FR-020**: System SHALL support real-time chart updates
|
||||
|
||||
### 1.5 Dashboard Module
|
||||
- **FR-021**: System SHALL provide an interactive dashboard interface
|
||||
- **FR-022**: System SHALL allow users to arrange and customize dashboard widgets
|
||||
- **FR-023**: System SHALL support multiple dashboard layouts
|
||||
- **FR-024**: System SHALL provide real-time data filtering capabilities
|
||||
- **FR-025**: System SHALL maintain dashboard state between sessions
|
||||
|
||||
### 1.6 Export Module
|
||||
- **FR-026**: System SHALL export analysis results as PDF files
|
||||
- **FR-027**: System SHALL export processed data as Excel files
|
||||
- **FR-028**: System SHALL include charts and visualizations in exports
|
||||
- **FR-029**: System SHALL provide export progress indication
|
||||
- **FR-030**: System SHALL support batch export functionality
|
||||
|
||||
## 2. Non-Functional Requirements
|
||||
|
||||
### 2.1 Performance Requirements
|
||||
- **NFR-001**: System SHALL load and process files up to 10,000 rows in under 10 seconds
|
||||
- **NFR-002**: System SHALL handle concurrent user sessions (up to 100 users)
|
||||
- **NFR-003**: Dashboard updates SHALL respond within 2 seconds
|
||||
- **NFR-004**: Chart rendering SHALL complete within 3 seconds
|
||||
|
||||
### 2.2 Usability Requirements
|
||||
- **NFR-005**: System SHALL be usable by users with basic computer skills
|
||||
- **NFR-006**: Learning curve SHALL be under 15 minutes for basic functionality
|
||||
- **NFR-007**: System SHALL provide clear error messages and help documentation
|
||||
- **NFR-008**: Interface SHALL be responsive and work on various screen sizes
|
||||
|
||||
### 2.3 Security Requirements
|
||||
- **NFR-009**: System SHALL not store uploaded files permanently
|
||||
- **NFR-010**: Data processing SHALL occur client-side only
|
||||
- **NFR-011**: System SHALL validate all user inputs
|
||||
- **NFR-012**: No sensitive data SHALL be transmitted to external servers
|
||||
|
||||
### 2.4 Compatibility Requirements
|
||||
- **NFR-013**: System SHALL work in modern web browsers (Chrome, Firefox, Safari, Edge)
|
||||
- **NFR-014**: System SHALL support both Windows and macOS operating systems
|
||||
- **NFR-015**: System SHALL be accessible on mobile devices with limited functionality
|
||||
|
||||
## 3. System Constraints
|
||||
|
||||
### 3.1 Technical Constraints
|
||||
- **C-001**: Application MUST run entirely in the browser (no server required)
|
||||
- **C-002**: File processing MUST use client-side JavaScript libraries
|
||||
- **C-003**: Application MUST work offline after initial load
|
||||
- **C-004**: Total application size MUST be under 5MB
|
||||
|
||||
### 3.2 Business Constraints
|
||||
- **C-005**: Development MUST use open-source technologies
|
||||
- **C-006**: Application MUST be free to use
|
||||
- **C-007**: No user registration or authentication required
|
||||
- **C-008**: Application MUST comply with data privacy regulations
|
||||
|
||||
## 4. User Interface Requirements
|
||||
|
||||
### 4.1 Layout Requirements
|
||||
- **UI-001**: Application SHALL have a clean, intuitive interface
|
||||
- **UI-002**: Main workspace SHALL be divided into logical sections
|
||||
- **UI-003**: Navigation SHALL be consistent across all pages
|
||||
- **UI-004**: Color scheme SHALL be professional and accessible
|
||||
|
||||
### 4.2 Interaction Requirements
|
||||
- **UI-005**: All interactive elements SHALL provide visual feedback
|
||||
- **UI-006**: Keyboard navigation SHALL be supported
|
||||
- **UI-007**: Touch interactions SHALL work on mobile devices
|
||||
- **UI-008**: Loading states SHALL be clearly indicated
|
||||
|
||||
## 5. Data Requirements
|
||||
|
||||
### 5.1 Input Data
|
||||
- **D-001**: Excel files (.xlsx, .xls) with maximum 10,000 rows
|
||||
- **D-002**: Data types: text, numbers, dates, boolean values
|
||||
- **D-003**: Multiple worksheets support (primary focus on first sheet)
|
||||
- **D-004**: File size limit: 50MB maximum
|
||||
|
||||
### 5.2 Output Data
|
||||
- **D-005**: Processed data in JSON format for internal use
|
||||
- **D-006**: PDF reports with charts and analysis summary
|
||||
- **D-007**: Excel files with cleaned and processed data
|
||||
- **D-008**: Chart configurations and dashboard layouts
|
||||
|
||||
## 6. Quality Attributes
|
||||
|
||||
### 6.1 Reliability
|
||||
- **QA-001**: System SHALL handle file parsing errors gracefully
|
||||
- **QA-002**: Data processing SHALL be consistent across browser sessions
|
||||
- **QA-003**: System SHALL recover from JavaScript errors without crashing
|
||||
- **QA-004**: Export functionality SHALL produce valid files
|
||||
|
||||
### 6.2 Maintainability
|
||||
- **QA-005**: Code SHALL be well-documented and modular
|
||||
- **QA-006**: System SHALL use established JavaScript libraries
|
||||
- **QA-007**: Components SHALL be easily testable
|
||||
- **QA-008**: Code SHALL follow consistent naming conventions
|
||||
|
||||
### 6.3 Scalability
|
||||
- **QA-009**: System SHALL handle increasing data complexity
|
||||
- **QA-010**: Visualization components SHALL be extensible
|
||||
- **QA-011**: New chart types SHALL be easily added
|
||||
- **QA-012**: Dashboard widgets SHALL be modular and reusable
|
||||
|
|
@ -0,0 +1,737 @@
|
|||
/* Excel Analyzer - Main Styles of Web Application */
|
||||
|
||||
/* CSS Custom Properties for Theming — Purple, Gold, Black & White Palette */
|
||||
:root {
|
||||
--primary-color: #6a0dad; /* Deep Royal Purple */
|
||||
--secondary-color: #FFD700; /* Gold */
|
||||
--success-color: #4caf50;
|
||||
--danger-color: #e53935;
|
||||
--warning-color: #FFD700; /* Gold as warning accent */
|
||||
--info-color: #ab47bc; /* Light Purple */
|
||||
--light-color: #ffffff;
|
||||
--dark-color: #0d0d0d; /* Near Black */
|
||||
|
||||
--bg-primary: #ffffff; /* White background */
|
||||
--bg-secondary: #f5f0ff; /* Very light purple tint */
|
||||
--bg-accent: #ede7f6; /* Soft lavender accent */
|
||||
|
||||
--text-primary: #0d0d0d; /* Near Black text */
|
||||
--text-secondary: #4a148c; /* Dark Purple text */
|
||||
--text-muted: #7b5ea7; /* Muted purple */
|
||||
|
||||
--border-color: #ce93d8; /* Light purple border */
|
||||
--shadow: 0 0.125rem 0.25rem rgba(106, 13, 173, 0.15);
|
||||
--shadow-lg: 0 0.5rem 1rem rgba(106, 13, 173, 0.25);
|
||||
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-size-base: 1rem;
|
||||
--line-height-base: 1.5;
|
||||
--border-radius: 0.375rem;
|
||||
--border-radius-lg: 0.5rem;
|
||||
--border-radius-sm: 0.25rem;
|
||||
|
||||
/* Gold accent for highlights */
|
||||
--gold-color: #FFD700;
|
||||
--gold-dark: #c9a800;
|
||||
--purple-dark: #4a148c;
|
||||
--purple-mid: #6a0dad;
|
||||
--purple-light: #ab47bc;
|
||||
}
|
||||
|
||||
/* Dark Theme — Black, Purple & Gold */
|
||||
[data-theme="dark"] {
|
||||
--primary-color: #ce93d8; /* Light Purple on dark */
|
||||
--secondary-color: #FFD700; /* Gold */
|
||||
--success-color: #66bb6a;
|
||||
--danger-color: #ef5350;
|
||||
--warning-color: #FFD700;
|
||||
--info-color: #ba68c8;
|
||||
|
||||
--bg-primary: #0d0d0d; /* True Black */
|
||||
--bg-secondary: #1a0a2e; /* Very Dark Purple */
|
||||
--bg-accent: #2d1b4e; /* Dark Purple accent */
|
||||
|
||||
--text-primary: #ffffff; /* White text */
|
||||
--text-secondary: #FFD700; /* Gold text */
|
||||
--text-muted: #ce93d8; /* Light purple muted */
|
||||
|
||||
--border-color: #4a148c; /* Dark purple border */
|
||||
--shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.6);
|
||||
--shadow-lg: 0 0.5rem 1rem rgba(206, 147, 216, 0.2);
|
||||
}
|
||||
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-primary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.text-muted {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--success-color) !important;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--danger-color) !important;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--warning-color) !important;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: var(--info-color) !important;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
background-color: var(--bg-primary);
|
||||
background-clip: border-box;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow);
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 0;
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-top-left-radius: calc(var(--border-radius) - 1px);
|
||||
border-top-right-radius: calc(var(--border-radius) - 1px);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1 1 auto;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 1rem 1.25rem;
|
||||
background-color: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom-right-radius: calc(var(--border-radius) - 1px);
|
||||
border-bottom-left-radius: calc(var(--border-radius) - 1px);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
color: #fff;
|
||||
background-color: var(--success-color);
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: #fff;
|
||||
background-color: var(--danger-color);
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
color: #212529;
|
||||
background-color: var(--warning-color);
|
||||
border-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
color: #fff;
|
||||
background-color: var(--info-color);
|
||||
border-color: var(--info-color);
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-primary);
|
||||
background-clip: padding-box;
|
||||
border: 1px solid var(--border-color);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border-radius: var(--border-radius);
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--primary-color);
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color), 0.25);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
|
||||
-moz-padding-start: calc(0.75rem - 3px);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-primary);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
background-size: 16px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color), 0.25);
|
||||
}
|
||||
|
||||
/* Progress Bars */
|
||||
.progress {
|
||||
display: flex;
|
||||
height: 1rem;
|
||||
overflow: hidden;
|
||||
font-size: 0.75rem;
|
||||
background-color: var(--bg-accent);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
background-color: var(--primary-color);
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table {
|
||||
--bs-table-bg: transparent;
|
||||
--bs-table-accent-bg: var(--bg-secondary);
|
||||
--bs-table-striped-color: var(--text-primary);
|
||||
--bs-table-striped-bg: var(--bg-accent);
|
||||
--bs-table-active-color: var(--text-primary);
|
||||
--bs-table-active-bg: var(--bg-accent);
|
||||
--bs-table-hover-color: var(--text-primary);
|
||||
--bs-table-hover-bg: var(--bg-accent);
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-primary);
|
||||
vertical-align: top;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.table > :not(caption) > * > * {
|
||||
padding: 0.5rem 0.5rem;
|
||||
background-color: var(--bs-table-bg);
|
||||
border-bottom-width: 1px;
|
||||
box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);
|
||||
}
|
||||
|
||||
.table-striped > tbody > tr:nth-of-type(odd) > * {
|
||||
--bs-table-accent-bg: var(--bs-table-striped-bg);
|
||||
color: var(--bs-table-striped-color);
|
||||
}
|
||||
|
||||
.table-hover > tbody > tr:hover > * {
|
||||
--bs-table-accent-bg: var(--bs-table-hover-bg);
|
||||
color: var(--bs-table-hover-color);
|
||||
}
|
||||
|
||||
/* Navigation — Purple & Gold */
|
||||
.navbar {
|
||||
background: linear-gradient(135deg, #4a148c 0%, #6a0dad 60%, #0d0d0d 100%) !important;
|
||||
box-shadow: 0 2px 12px rgba(106, 13, 173, 0.4);
|
||||
border-bottom: 3px solid #FFD700;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
font-size: 1.35rem;
|
||||
color: #FFD700 !important;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 1px 4px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: rgba(255, 255, 255, 0.85) !important;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
transition: all 0.2s ease-in-out;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: #FFD700 !important;
|
||||
background-color: rgba(255, 215, 0, 0.1);
|
||||
border-bottom-color: #FFD700;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: #FFD700 !important;
|
||||
background-color: rgba(255, 215, 0, 0.15);
|
||||
border-bottom-color: #FFD700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Card Headers — Purple accent */
|
||||
.card-header {
|
||||
background: linear-gradient(90deg, var(--purple-dark) 0%, var(--purple-mid) 100%);
|
||||
color: #ffffff;
|
||||
border-bottom: 2px solid var(--gold-color);
|
||||
}
|
||||
|
||||
.card-header h2,
|
||||
.card-header h3,
|
||||
.card-header h4,
|
||||
.card-header h5,
|
||||
.card-header h6 {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.card-header .btn-outline-primary {
|
||||
color: #FFD700;
|
||||
border-color: #FFD700;
|
||||
}
|
||||
|
||||
.card-header .btn-outline-primary:hover {
|
||||
background-color: #FFD700;
|
||||
color: #0d0d0d;
|
||||
}
|
||||
|
||||
.card-header .btn-outline-secondary {
|
||||
color: #ffffff;
|
||||
border-color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.card-header .btn-outline-secondary:hover {
|
||||
background-color: rgba(255,255,255,0.15);
|
||||
color: #FFD700;
|
||||
border-color: #FFD700;
|
||||
}
|
||||
|
||||
/* Drop Zone — Purple & Gold */
|
||||
.drop-zone {
|
||||
border: 2px dashed var(--purple-light) !important;
|
||||
background-color: var(--bg-secondary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.drop-zone:hover,
|
||||
.drop-zone.drag-over {
|
||||
border-color: var(--gold-color) !important;
|
||||
background-color: rgba(255, 215, 0, 0.05);
|
||||
box-shadow: 0 0 20px rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Primary Button — Purple with Gold hover */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #6a0dad, #4a148c);
|
||||
border-color: #6a0dad;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus {
|
||||
background: linear-gradient(135deg, #FFD700, #c9a800);
|
||||
border-color: #FFD700;
|
||||
color: #0d0d0d;
|
||||
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Footer — Black with Gold & Purple accents */
|
||||
footer {
|
||||
background: linear-gradient(135deg, #0d0d0d 0%, #1a0a2e 100%) !important;
|
||||
border-top: 3px solid #FFD700;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
footer h5 {
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
footer p {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
footer .text-muted {
|
||||
color: var(--purple-light) !important;
|
||||
}
|
||||
|
||||
/* Stats Cards — Purple tinted */
|
||||
.card.bg-light {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.card.bg-light .card-title {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Progress Bar — Gold fill */
|
||||
.progress-bar {
|
||||
background: linear-gradient(90deg, #6a0dad, #FFD700);
|
||||
}
|
||||
|
||||
/* Table Header — Purple */
|
||||
.table thead th {
|
||||
background-color: var(--purple-dark);
|
||||
color: #FFD700;
|
||||
border-color: var(--purple-mid);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Scrollbar — Purple & Gold */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--purple-mid);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--gold-color);
|
||||
}
|
||||
|
||||
/* Selection highlight */
|
||||
::selection {
|
||||
background-color: rgba(106, 13, 173, 0.25);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.bi {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -.125em;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.text-start {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.text-end {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.justify-content-between {
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 1rem !important;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid var(--border-color) !important;
|
||||
}
|
||||
|
||||
.border-dashed {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: var(--border-radius) !important;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: var(--shadow) !important;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: -0.125em;
|
||||
border: 0.25em solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: spinner-border .75s linear infinite;
|
||||
animation: spinner-border .75s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner-border {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Status Indicators */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: var(--border-radius-sm);
|
||||
background-color: var(--bg-accent);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: rgba(40, 167, 69, 0.1);
|
||||
color: var(--success-color);
|
||||
border: 1px solid rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background-color: rgba(255, 193, 7, 0.1);
|
||||
color: var(--warning-color);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.status-danger {
|
||||
background-color: rgba(220, 53, 69, 0.1);
|
||||
color: var(--danger-color);
|
||||
border: 1px solid rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
body {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.navbar,
|
||||
.btn,
|
||||
.progress,
|
||||
.card-footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
border: 1px solid #000;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Hook Engine
|
||||
*
|
||||
* Lightweight hook system for registering and executing synchronous/asynchronous hooks
|
||||
* with priority and one-time execution support.
|
||||
*/
|
||||
|
||||
class HookEngine {
|
||||
constructor() {
|
||||
this.hooks = new Map();
|
||||
this.sequence = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a hook handler.
|
||||
* @param {string} hookName
|
||||
* @param {Function} handler
|
||||
* @param {{ priority?: number, once?: boolean }} options
|
||||
* @returns {string} Hook id
|
||||
*/
|
||||
tap(hookName, handler, options = {}) {
|
||||
if (!hookName || typeof hookName !== 'string') {
|
||||
throw new Error('hookName must be a non-empty string');
|
||||
}
|
||||
|
||||
if (typeof handler !== 'function') {
|
||||
throw new Error('handler must be a function');
|
||||
}
|
||||
|
||||
const priority = Number.isFinite(options.priority) ? options.priority : 0;
|
||||
const once = Boolean(options.once);
|
||||
const id = `${hookName}:${Date.now()}:${this.sequence++}`;
|
||||
|
||||
const record = {
|
||||
id,
|
||||
hookName,
|
||||
handler,
|
||||
priority,
|
||||
once,
|
||||
order: this.sequence
|
||||
};
|
||||
|
||||
if (!this.hooks.has(hookName)) {
|
||||
this.hooks.set(hookName, []);
|
||||
}
|
||||
|
||||
const items = this.hooks.get(hookName);
|
||||
items.push(record);
|
||||
items.sort((a, b) => {
|
||||
if (b.priority !== a.priority) return b.priority - a.priority;
|
||||
return a.order - b.order;
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a one-time hook handler.
|
||||
*/
|
||||
tapOnce(hookName, handler, options = {}) {
|
||||
return this.tap(hookName, handler, { ...options, once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific hook by id.
|
||||
*/
|
||||
untap(hookName, hookId) {
|
||||
const items = this.hooks.get(hookName);
|
||||
if (!items || !items.length) return false;
|
||||
|
||||
const next = items.filter((entry) => entry.id !== hookId);
|
||||
this.hooks.set(hookName, next);
|
||||
return next.length !== items.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all handlers for a hook, or all hooks when no name provided.
|
||||
*/
|
||||
clear(hookName = null) {
|
||||
if (!hookName) {
|
||||
this.hooks.clear();
|
||||
return;
|
||||
}
|
||||
this.hooks.delete(hookName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a hook has handlers.
|
||||
*/
|
||||
has(hookName) {
|
||||
const items = this.hooks.get(hookName);
|
||||
return Boolean(items && items.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* List registered handlers for a hook.
|
||||
*/
|
||||
list(hookName) {
|
||||
return [...(this.hooks.get(hookName) || [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all handlers for a hook with a shared context object.
|
||||
* Returns collected results and errors.
|
||||
*/
|
||||
async run(hookName, context = {}) {
|
||||
const handlers = [...(this.hooks.get(hookName) || [])];
|
||||
const results = [];
|
||||
const errors = [];
|
||||
|
||||
for (const entry of handlers) {
|
||||
try {
|
||||
const value = await entry.handler(context);
|
||||
results.push({ id: entry.id, value });
|
||||
} catch (error) {
|
||||
errors.push({ id: entry.id, error });
|
||||
} finally {
|
||||
if (entry.once) {
|
||||
this.untap(hookName, entry.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { results, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute hook handlers in waterfall mode.
|
||||
* Each handler can transform the payload and pass to the next handler.
|
||||
*/
|
||||
async runWaterfall(hookName, payload, context = {}) {
|
||||
const handlers = [...(this.hooks.get(hookName) || [])];
|
||||
let current = payload;
|
||||
|
||||
for (const entry of handlers) {
|
||||
try {
|
||||
const next = await entry.handler(current, context);
|
||||
if (typeof next !== 'undefined') {
|
||||
current = next;
|
||||
}
|
||||
} finally {
|
||||
if (entry.once) {
|
||||
this.untap(hookName, entry.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = HookEngine;
|
||||
} else {
|
||||
window.HookEngine = HookEngine;
|
||||
}
|
||||
338
apps/web-roo-code/src/components/excel-analyzer/src/index.html
Normal file
338
apps/web-roo-code/src/components/excel-analyzer/src/index.html
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Excel Analyzer - Web-based Data Analysis Tool</title>
|
||||
|
||||
<!-- External Libraries -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<!-- Application Styles -->
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation Bar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#">
|
||||
<i class="bi bi-file-earmark-spreadsheet me-2"></i>
|
||||
Excel Analyzer
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#upload" data-section="upload">Upload</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#preview" data-section="preview">Preview</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#analyze" data-section="analyze">Analyze</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#dashboard" data-section="dashboard">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#export" data-section="export">Export</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button id="themeToggle" class="btn btn-sm btn-outline-light" type="button" title="Toggle Theme">
|
||||
<i class="bi bi-moon-stars"></i>
|
||||
</button>
|
||||
<span class="navbar-text d-none d-lg-inline">
|
||||
Client-side processing - Your data stays private
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container my-4">
|
||||
<!-- File Upload Section -->
|
||||
<section id="upload" class="upload-section mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-upload"></i>
|
||||
Upload Excel File
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dropZone" class="drop-zone text-center p-4 border border-dashed rounded">
|
||||
<div class="drop-zone-content">
|
||||
<i class="bi bi-file-earmark-excel display-4 text-muted mb-3"></i>
|
||||
<h5>Drag and drop your Excel file here</h5>
|
||||
<p class="text-muted">or</p>
|
||||
<input type="file" id="fileInput" accept=".xlsx,.xls" class="form-control" style="display: none;">
|
||||
<button type="button" id="selectFileBtn" class="btn btn-primary">
|
||||
Select File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fileInfo" class="mt-3" style="display: none;">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<strong>File:</strong> <span id="fileName"></span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>Size:</strong> <span id="fileSize"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="uploadProgress" class="progress mt-3" style="display: none;">
|
||||
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
|
||||
<div id="uploadStatus" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Data Preview Section -->
|
||||
<section id="preview" class="preview-section mb-4" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-table"></i>
|
||||
Data Preview
|
||||
</h2>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary" id="btnFirst100">First 100 Rows</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnAllData">All Data</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">Total Rows</h6>
|
||||
<h3 class="text-primary" id="totalRows">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">Total Columns</h6>
|
||||
<h3 class="text-primary" id="totalCols">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">Missing Values</h6>
|
||||
<h3 class="text-warning" id="missingValues">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title">Data Quality</h6>
|
||||
<h3 class="text-success" id="dataQuality">100%</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover" id="dataTable">
|
||||
<thead id="tableHeader">
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Analysis Section -->
|
||||
<section id="analyze" class="analyze-section mb-4" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-bar-chart"></i>
|
||||
Data Analysis
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Column Analysis</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="columnAnalysis"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5>Chart Selection</h5>
|
||||
<select id="chartTypeSelector" class="form-select" style="width: auto;">
|
||||
<option value="auto">Auto Select</option>
|
||||
<option value="bar">Bar Chart</option>
|
||||
<option value="line">Line Chart</option>
|
||||
<option value="pie">Pie Chart</option>
|
||||
<option value="scatter">Scatter Plot</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label for="xAxisSelector" class="form-label">X-Axis</label>
|
||||
<select id="xAxisSelector" class="form-select"></select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="yAxisSelector" class="form-label">Y-Axis</label>
|
||||
<select id="yAxisSelector" class="form-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<canvas id="analysisChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dashboard Section -->
|
||||
<section id="dashboard" class="dashboard-section mb-4" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-columns"></i>
|
||||
Interactive Dashboard
|
||||
</h2>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary" id="layout1">Layout 1</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="layout2">Layout 2</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="layout3">Layout 3</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="dashboard-grid" id="dashboardGrid">
|
||||
<!-- Dashboard widgets will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Export Section -->
|
||||
<section id="export" class="export-section mb-4" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">
|
||||
<i class="bi bi-download"></i>
|
||||
Export Results
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-file-pdf display-4 text-danger mb-3"></i>
|
||||
<h5>PDF Report</h5>
|
||||
<p class="text-muted">Export analysis with charts</p>
|
||||
<button class="btn btn-danger" id="exportPDF">
|
||||
<i class="bi bi-download"></i> Download PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-file-earmark-excel display-4 text-success mb-3"></i>
|
||||
<h5>Excel File</h5>
|
||||
<p class="text-muted">Export processed data</p>
|
||||
<button class="btn btn-success" id="exportExcel">
|
||||
<i class="bi bi-download"></i> Download Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-image display-4 text-info mb-3"></i>
|
||||
<h5>Chart Images</h5>
|
||||
<p class="text-muted">Export individual charts</p>
|
||||
<button class="btn btn-info" id="exportCharts">
|
||||
<i class="bi bi-download"></i> Export Charts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="progress" id="exportProgress" style="display: none;">
|
||||
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div id="exportStatus" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-dark text-light py-4">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Excel Analyzer</h5>
|
||||
<p>A powerful web-based tool for Excel data analysis and visualization.</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<p>© 2026 Excel Analyzer. All rights reserved.</p>
|
||||
<p class="small text-muted">Client-side processing ensures your data privacy</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- External Libraries -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
|
||||
<!-- Application Scripts -->
|
||||
<script src="js/config/defaults.js"></script>
|
||||
<script src="js/config/themes.js"></script>
|
||||
<script src="js/config/settings.js"></script>
|
||||
<script src="js/modules/file-upload.js"></script>
|
||||
<script src="js/modules/data-processor.js"></script>
|
||||
<script src="js/modules/visualizer.js"></script>
|
||||
<script src="js/modules/dashboard.js"></script>
|
||||
<script src="js/modules/exporter.js"></script>
|
||||
<script src="js/modules/notifications.js"></script>
|
||||
<script src="js/utils/helpers.js"></script>
|
||||
<script src="js/utils/validators.js"></script>
|
||||
<script src="js/utils/formatters.js"></script>
|
||||
<script src="js/events/event-system.js"></script>
|
||||
<script src="js/events/event-handlers.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,506 @@
|
|||
/**
|
||||
* Default Values of Web Application
|
||||
*
|
||||
* Default configuration values and presets for the Excel Analyzer application.
|
||||
*/
|
||||
|
||||
class DefaultValues {
|
||||
constructor() {
|
||||
this.defaults = {
|
||||
// Application Defaults
|
||||
app: {
|
||||
name: 'Excel Analyzer',
|
||||
version: '1.0.0',
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
maxFileSize: 50 * 1024 * 1024, // 50MB
|
||||
maxRows: 10000,
|
||||
maxColumns: 100
|
||||
},
|
||||
|
||||
// File Upload Defaults
|
||||
fileUpload: {
|
||||
allowedTypes: [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel'
|
||||
],
|
||||
maxFileSize: 50 * 1024 * 1024,
|
||||
enableDragDrop: true,
|
||||
enableMultiple: false,
|
||||
previewRows: 100
|
||||
},
|
||||
|
||||
// Data Processing Defaults
|
||||
dataProcessing: {
|
||||
defaultMissingValueHandling: 'remove',
|
||||
defaultOutlierHandling: 'remove',
|
||||
outlierThreshold: 3,
|
||||
autoDetectDataTypes: true,
|
||||
enableDataValidation: true,
|
||||
maxPreviewRows: 100,
|
||||
enableCaching: true,
|
||||
cacheTimeout: 300000 // 5 minutes
|
||||
},
|
||||
|
||||
// Visualization Defaults
|
||||
visualization: {
|
||||
defaultChartType: 'auto',
|
||||
chartColors: [
|
||||
'#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8',
|
||||
'#6f42c1', '#20c997', '#fd7e14', '#e83e8c', '#6c757d'
|
||||
],
|
||||
chartAnimation: true,
|
||||
chartResponsive: true,
|
||||
chartLegendPosition: 'top',
|
||||
chartTooltip: true,
|
||||
chartHeight: 400,
|
||||
chartWidth: 600,
|
||||
enableZoom: true,
|
||||
enablePan: true
|
||||
},
|
||||
|
||||
// Dashboard Defaults
|
||||
dashboard: {
|
||||
defaultLayout: 'grid',
|
||||
maxWidgets: 10,
|
||||
enableWidgetResize: true,
|
||||
enableWidgetDrag: true,
|
||||
autoSaveDashboard: true,
|
||||
defaultWidgetSize: {
|
||||
width: 4,
|
||||
height: 3
|
||||
},
|
||||
gridColumns: 12,
|
||||
gridRows: 12,
|
||||
widgetMargin: 10
|
||||
},
|
||||
|
||||
// Export Defaults
|
||||
export: {
|
||||
defaultFormat: 'pdf',
|
||||
includeChartsInExport: true,
|
||||
includeDataInExport: true,
|
||||
includeSummaryInExport: true,
|
||||
exportImageQuality: 'high',
|
||||
pdfPageSize: 'A4',
|
||||
pdfOrientation: 'portrait',
|
||||
excelSheetName: 'Processed Data',
|
||||
csvDelimiter: ',',
|
||||
jsonIndent: 2
|
||||
},
|
||||
|
||||
// UI Defaults
|
||||
ui: {
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
enableTooltips: true,
|
||||
enableAnimations: true,
|
||||
fontSize: 'medium',
|
||||
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial',
|
||||
borderRadius: '0.375rem',
|
||||
boxShadow: '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
||||
transitionDuration: '0.15s'
|
||||
},
|
||||
|
||||
// Performance Defaults
|
||||
performance: {
|
||||
enableVirtualization: true,
|
||||
maxPreviewRows: 100,
|
||||
enableCaching: true,
|
||||
cacheTimeout: 300000, // 5 minutes
|
||||
debounceDelay: 300,
|
||||
throttleDelay: 1000,
|
||||
maxConcurrentOperations: 3
|
||||
},
|
||||
|
||||
// Privacy Defaults
|
||||
privacy: {
|
||||
enableAnalytics: false,
|
||||
saveUserPreferences: true,
|
||||
clearDataOnExit: false,
|
||||
storeFileData: false,
|
||||
storeProcessingHistory: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default value by path
|
||||
*/
|
||||
get(path, defaultValue = null) {
|
||||
const keys = path.split('.');
|
||||
let current = this.defaults;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
current = current[key];
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all defaults
|
||||
*/
|
||||
getAll() {
|
||||
return { ...this.defaults };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application defaults
|
||||
*/
|
||||
getAppDefaults() {
|
||||
return { ...this.defaults.app };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file upload defaults
|
||||
*/
|
||||
getFileUploadDefaults() {
|
||||
return { ...this.defaults.fileUpload };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data processing defaults
|
||||
*/
|
||||
getDataProcessingDefaults() {
|
||||
return { ...this.defaults.dataProcessing };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visualization defaults
|
||||
*/
|
||||
getVisualizationDefaults() {
|
||||
return { ...this.defaults.visualization };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dashboard defaults
|
||||
*/
|
||||
getDashboardDefaults() {
|
||||
return { ...this.defaults.dashboard };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get export defaults
|
||||
*/
|
||||
getExportDefaults() {
|
||||
return { ...this.defaults.export };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UI defaults
|
||||
*/
|
||||
getUIDefaults() {
|
||||
return { ...this.defaults.ui };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance defaults
|
||||
*/
|
||||
getPerformanceDefaults() {
|
||||
return { ...this.defaults.performance };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get privacy defaults
|
||||
*/
|
||||
getPrivacyDefaults() {
|
||||
return { ...this.defaults.privacy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chart color presets
|
||||
*/
|
||||
getChartColorPresets() {
|
||||
return {
|
||||
primary: ['#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8'],
|
||||
pastel: ['#a8d8f0', '#b8e6b8', '#ffd1d1', '#fff7b3', '#b3e6ff'],
|
||||
vibrant: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7'],
|
||||
monochrome: ['#333333', '#666666', '#999999', '#cccccc', '#ffffff'],
|
||||
autumn: ['#d35400', '#e67e22', '#f1c40f', '#f39c12', '#e74c3c']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chart type presets
|
||||
*/
|
||||
getChartTypePresets() {
|
||||
return {
|
||||
auto: 'Automatic selection based on data',
|
||||
bar: 'Bar chart for categorical data',
|
||||
line: 'Line chart for time series data',
|
||||
pie: 'Pie chart for proportional data',
|
||||
scatter: 'Scatter plot for correlation analysis',
|
||||
doughnut: 'Doughnut chart for proportional data',
|
||||
area: 'Area chart for cumulative data',
|
||||
radar: 'Radar chart for multi-dimensional data'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dashboard layout presets
|
||||
*/
|
||||
getDashboardLayoutPresets() {
|
||||
return {
|
||||
grid: {
|
||||
name: 'Grid Layout',
|
||||
description: 'Responsive grid-based layout',
|
||||
columns: 12,
|
||||
rows: 12,
|
||||
margin: 10,
|
||||
autoResize: true
|
||||
},
|
||||
vertical: {
|
||||
name: 'Vertical Layout',
|
||||
description: 'Stacked widgets vertically',
|
||||
columns: 1,
|
||||
rows: 20,
|
||||
margin: 15,
|
||||
autoResize: false
|
||||
},
|
||||
horizontal: {
|
||||
name: 'Horizontal Layout',
|
||||
description: 'Side-by-side widgets',
|
||||
columns: 20,
|
||||
rows: 6,
|
||||
margin: 10,
|
||||
autoResize: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get export format presets
|
||||
*/
|
||||
getExportFormatPresets() {
|
||||
return {
|
||||
pdf: {
|
||||
name: 'PDF Report',
|
||||
description: 'Professional PDF with charts and analysis',
|
||||
extension: '.pdf',
|
||||
includeCharts: true,
|
||||
includeData: true,
|
||||
includeSummary: true
|
||||
},
|
||||
excel: {
|
||||
name: 'Excel File',
|
||||
description: 'Processed data in Excel format',
|
||||
extension: '.xlsx',
|
||||
includeCharts: false,
|
||||
includeData: true,
|
||||
includeSummary: false
|
||||
},
|
||||
csv: {
|
||||
name: 'CSV File',
|
||||
description: 'Comma-separated values',
|
||||
extension: '.csv',
|
||||
includeCharts: false,
|
||||
includeData: true,
|
||||
includeSummary: false
|
||||
},
|
||||
json: {
|
||||
name: 'JSON File',
|
||||
description: 'Structured data in JSON format',
|
||||
extension: '.json',
|
||||
includeCharts: false,
|
||||
includeData: true,
|
||||
includeSummary: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme presets
|
||||
*/
|
||||
getThemePresets() {
|
||||
return {
|
||||
light: {
|
||||
name: 'Light Theme',
|
||||
description: 'Clean and bright interface',
|
||||
colors: {
|
||||
primary: '#007bff',
|
||||
secondary: '#6c757d',
|
||||
success: '#28a745',
|
||||
danger: '#dc3545',
|
||||
warning: '#ffc107',
|
||||
info: '#17a2b8',
|
||||
light: '#f8f9fa',
|
||||
dark: '#343a40'
|
||||
},
|
||||
background: '#ffffff',
|
||||
text: '#212529'
|
||||
},
|
||||
dark: {
|
||||
name: 'Dark Theme',
|
||||
description: 'Dark interface for reduced eye strain',
|
||||
colors: {
|
||||
primary: '#4dabf7',
|
||||
secondary: '#66d9ef',
|
||||
success: '#51cf66',
|
||||
danger: '#ff6b6b',
|
||||
warning: '#ffd43b',
|
||||
info: '#22d3ee',
|
||||
light: '#121212',
|
||||
dark: '#ffffff'
|
||||
},
|
||||
background: '#121212',
|
||||
text: '#ffffff'
|
||||
},
|
||||
blue: {
|
||||
name: 'Blue Theme',
|
||||
description: 'Professional blue color scheme',
|
||||
colors: {
|
||||
primary: '#1976d2',
|
||||
secondary: '#424242',
|
||||
success: '#2e7d32',
|
||||
danger: '#c62828',
|
||||
warning: '#ef6c00',
|
||||
info: '#0277bd',
|
||||
light: '#e3f2fd',
|
||||
dark: '#212121'
|
||||
},
|
||||
background: '#ffffff',
|
||||
text: '#212121'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data type detection rules
|
||||
*/
|
||||
getDataTypeDetectionRules() {
|
||||
return {
|
||||
date: {
|
||||
patterns: [
|
||||
/^\d{4}-\d{2}-\d{2}$/,
|
||||
/^\d{2}\/\d{2}\/\d{4}$/,
|
||||
/^\d{2}-\d{2}-\d{4}$/,
|
||||
/^\d{4}\/\d{2}\/\d{2}$/
|
||||
],
|
||||
validators: [
|
||||
(value) => !isNaN(Date.parse(value))
|
||||
]
|
||||
},
|
||||
time: {
|
||||
patterns: [
|
||||
/^\d{2}:\d{2}:\d{2}$/,
|
||||
/^\d{2}:\d{2}$/
|
||||
],
|
||||
validators: [
|
||||
(value) => {
|
||||
const time = value.split(':');
|
||||
return time.length >= 2 &&
|
||||
parseInt(time[0]) >= 0 && parseInt(time[0]) <= 23 &&
|
||||
parseInt(time[1]) >= 0 && parseInt(time[1]) <= 59;
|
||||
}
|
||||
]
|
||||
},
|
||||
numeric: {
|
||||
patterns: [
|
||||
/^-?\d+\.?\d*$/
|
||||
],
|
||||
validators: [
|
||||
(value) => !isNaN(parseFloat(value)) && isFinite(value)
|
||||
]
|
||||
},
|
||||
boolean: {
|
||||
patterns: [
|
||||
/^(true|false|yes|no|1|0)$/i
|
||||
],
|
||||
validators: [
|
||||
(value) => ['true', 'false', 'yes', 'no', '1', '0'].includes(value.toLowerCase())
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get processing strategies
|
||||
*/
|
||||
getProcessingStrategies() {
|
||||
return {
|
||||
missingValues: {
|
||||
remove: 'Remove rows with missing values',
|
||||
mean: 'Fill with mean (numeric only)',
|
||||
median: 'Fill with median (numeric only)',
|
||||
mode: 'Fill with most frequent value',
|
||||
forwardFill: 'Forward fill from previous value',
|
||||
backwardFill: 'Backward fill from next value',
|
||||
custom: 'Use custom value'
|
||||
},
|
||||
outliers: {
|
||||
remove: 'Remove outlier values',
|
||||
cap: 'Cap to threshold values',
|
||||
mean: 'Replace with mean',
|
||||
median: 'Replace with median',
|
||||
ignore: 'Keep outliers as-is'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation rules
|
||||
*/
|
||||
getValidationRules() {
|
||||
return {
|
||||
file: {
|
||||
maxSize: 50 * 1024 * 1024, // 50MB
|
||||
allowedTypes: [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel'
|
||||
],
|
||||
maxRows: 10000,
|
||||
maxColumns: 100
|
||||
},
|
||||
data: {
|
||||
maxPreviewRows: 100,
|
||||
maxCells: 1000000,
|
||||
maxStringLength: 10000
|
||||
},
|
||||
charts: {
|
||||
maxDataPoints: 1000,
|
||||
maxSeries: 10,
|
||||
maxLabels: 50
|
||||
},
|
||||
dashboard: {
|
||||
maxWidgets: 20,
|
||||
minWidgetSize: { width: 2, height: 2 },
|
||||
maxWidgetSize: { width: 12, height: 12 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance thresholds
|
||||
*/
|
||||
getPerformanceThresholds() {
|
||||
return {
|
||||
fileProcessing: {
|
||||
small: 1000, // rows
|
||||
medium: 10000, // rows
|
||||
large: 50000 // rows
|
||||
},
|
||||
chartRendering: {
|
||||
fast: 100, // data points
|
||||
medium: 1000, // data points
|
||||
slow: 10000 // data points
|
||||
},
|
||||
memoryUsage: {
|
||||
low: 50 * 1024 * 1024, // 50MB
|
||||
medium: 100 * 1024 * 1024, // 100MB
|
||||
high: 500 * 1024 * 1024 // 500MB
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = DefaultValues;
|
||||
} else {
|
||||
window.DefaultValues = DefaultValues;
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* Settings Manager of Web Application
|
||||
*
|
||||
* Lightweight settings storage for the Excel Analyzer app.
|
||||
*/
|
||||
|
||||
class SettingsManager {
|
||||
constructor() {
|
||||
this.storageKey = 'excel-analyzer-settings';
|
||||
this.defaultSettings = {
|
||||
maxFileSize: 50 * 1024 * 1024,
|
||||
maxRows: 10000,
|
||||
previewRows: 100,
|
||||
theme: 'light',
|
||||
ui: {
|
||||
theme: 'light'
|
||||
},
|
||||
dashboard: {
|
||||
layout: 'layout1'
|
||||
},
|
||||
export: {
|
||||
includeCharts: true,
|
||||
includeData: true,
|
||||
includeSummary: true
|
||||
}
|
||||
};
|
||||
|
||||
this.settings = this.load();
|
||||
}
|
||||
|
||||
load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.storageKey);
|
||||
if (!raw) return this.clone(this.defaultSettings);
|
||||
const parsed = JSON.parse(raw);
|
||||
return this.merge(this.defaultSettings, parsed);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load settings, using defaults.', error);
|
||||
return this.clone(this.defaultSettings);
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(this.settings));
|
||||
} catch (error) {
|
||||
console.warn('Failed to save settings.', error);
|
||||
}
|
||||
}
|
||||
|
||||
get(path, fallback = null) {
|
||||
if (!path) return this.settings;
|
||||
const keys = path.split('.');
|
||||
let current = this.settings;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
current = current[key];
|
||||
} else {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
set(path, value) {
|
||||
const keys = path.split('.');
|
||||
let current = this.settings;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!current[key] || typeof current[key] !== 'object') {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
this.save();
|
||||
}
|
||||
|
||||
update(values = {}) {
|
||||
this.settings = this.merge(this.settings, values);
|
||||
this.save();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.settings = this.clone(this.defaultSettings);
|
||||
this.save();
|
||||
}
|
||||
|
||||
clone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
merge(base, extra) {
|
||||
const merged = this.clone(base);
|
||||
Object.keys(extra || {}).forEach((key) => {
|
||||
const baseValue = merged[key];
|
||||
const extraValue = extra[key];
|
||||
|
||||
if (
|
||||
baseValue &&
|
||||
typeof baseValue === 'object' &&
|
||||
!Array.isArray(baseValue) &&
|
||||
extraValue &&
|
||||
typeof extraValue === 'object' &&
|
||||
!Array.isArray(extraValue)
|
||||
) {
|
||||
merged[key] = this.merge(baseValue, extraValue);
|
||||
} else {
|
||||
merged[key] = extraValue;
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = SettingsManager;
|
||||
} else {
|
||||
window.SettingsManager = SettingsManager;
|
||||
}
|
||||
|
|
@ -0,0 +1,567 @@
|
|||
/**
|
||||
* Theme Manager of Web Application
|
||||
*
|
||||
* Manages theme switching and CSS custom properties for the Excel Analyzer application.
|
||||
*/
|
||||
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.themes = {
|
||||
royal: {
|
||||
name: '👑 Royal',
|
||||
description: 'Purple, Gold, Black & White — elegant royal palette',
|
||||
cssVariables: {
|
||||
'--primary-color': '#6a0dad',
|
||||
'--secondary-color': '#FFD700',
|
||||
'--success-color': '#4caf50',
|
||||
'--danger-color': '#e53935',
|
||||
'--warning-color': '#FFD700',
|
||||
'--info-color': '#ab47bc',
|
||||
'--light-color': '#ffffff',
|
||||
'--dark-color': '#0d0d0d',
|
||||
|
||||
'--bg-primary': '#ffffff',
|
||||
'--bg-secondary': '#f5f0ff',
|
||||
'--bg-accent': '#ede7f6',
|
||||
|
||||
'--text-primary': '#0d0d0d',
|
||||
'--text-secondary': '#4a148c',
|
||||
'--text-muted': '#7b5ea7',
|
||||
|
||||
'--border-color': '#ce93d8',
|
||||
'--shadow': '0 0.125rem 0.25rem rgba(106, 13, 173, 0.15)',
|
||||
'--shadow-lg': '0 0.5rem 1rem rgba(106, 13, 173, 0.25)',
|
||||
|
||||
'--gold-color': '#FFD700',
|
||||
'--gold-dark': '#c9a800',
|
||||
'--purple-dark': '#4a148c',
|
||||
'--purple-mid': '#6a0dad',
|
||||
'--purple-light': '#ab47bc',
|
||||
|
||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'--font-size-base': '1rem',
|
||||
'--line-height-base': '1.5',
|
||||
'--border-radius': '0.375rem',
|
||||
'--border-radius-lg': '0.5rem',
|
||||
'--border-radius-sm': '0.25rem'
|
||||
}
|
||||
},
|
||||
'royal-dark': {
|
||||
name: '🌑 Royal Dark',
|
||||
description: 'Black, Deep Purple & Gold — dark royal palette',
|
||||
cssVariables: {
|
||||
'--primary-color': '#ce93d8',
|
||||
'--secondary-color': '#FFD700',
|
||||
'--success-color': '#66bb6a',
|
||||
'--danger-color': '#ef5350',
|
||||
'--warning-color': '#FFD700',
|
||||
'--info-color': '#ba68c8',
|
||||
'--light-color': '#0d0d0d',
|
||||
'--dark-color': '#ffffff',
|
||||
|
||||
'--bg-primary': '#0d0d0d',
|
||||
'--bg-secondary': '#1a0a2e',
|
||||
'--bg-accent': '#2d1b4e',
|
||||
|
||||
'--text-primary': '#ffffff',
|
||||
'--text-secondary': '#FFD700',
|
||||
'--text-muted': '#ce93d8',
|
||||
|
||||
'--border-color': '#4a148c',
|
||||
'--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.6)',
|
||||
'--shadow-lg': '0 0.5rem 1rem rgba(206, 147, 216, 0.2)',
|
||||
|
||||
'--gold-color': '#FFD700',
|
||||
'--gold-dark': '#c9a800',
|
||||
'--purple-dark': '#4a148c',
|
||||
'--purple-mid': '#6a0dad',
|
||||
'--purple-light': '#ce93d8',
|
||||
|
||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'--font-size-base': '1rem',
|
||||
'--line-height-base': '1.5',
|
||||
'--border-radius': '0.375rem',
|
||||
'--border-radius-lg': '0.5rem',
|
||||
'--border-radius-sm': '0.25rem'
|
||||
}
|
||||
},
|
||||
light: {
|
||||
name: 'Light',
|
||||
description: 'Clean and bright interface',
|
||||
cssVariables: {
|
||||
'--primary-color': '#007bff',
|
||||
'--secondary-color': '#6c757d',
|
||||
'--success-color': '#28a745',
|
||||
'--danger-color': '#dc3545',
|
||||
'--warning-color': '#ffc107',
|
||||
'--info-color': '#17a2b8',
|
||||
'--light-color': '#f8f9fa',
|
||||
'--dark-color': '#343a40',
|
||||
|
||||
'--bg-primary': '#ffffff',
|
||||
'--bg-secondary': '#f8f9fa',
|
||||
'--bg-accent': '#e9ecef',
|
||||
|
||||
'--text-primary': '#212529',
|
||||
'--text-secondary': '#6c757d',
|
||||
'--text-muted': '#6c757d',
|
||||
|
||||
'--border-color': '#dee2e6',
|
||||
'--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
||||
'--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
|
||||
|
||||
'--gold-color': '#ffc107',
|
||||
'--gold-dark': '#e0a800',
|
||||
'--purple-dark': '#6f42c1',
|
||||
'--purple-mid': '#7b1fa2',
|
||||
'--purple-light': '#ab47bc',
|
||||
|
||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'--font-size-base': '1rem',
|
||||
'--line-height-base': '1.5',
|
||||
'--border-radius': '0.375rem',
|
||||
'--border-radius-lg': '0.5rem',
|
||||
'--border-radius-sm': '0.25rem'
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
name: 'Dark',
|
||||
description: 'Dark interface for reduced eye strain',
|
||||
cssVariables: {
|
||||
'--primary-color': '#4dabf7',
|
||||
'--secondary-color': '#66d9ef',
|
||||
'--success-color': '#51cf66',
|
||||
'--danger-color': '#ff6b6b',
|
||||
'--warning-color': '#ffd43b',
|
||||
'--info-color': '#22d3ee',
|
||||
'--light-color': '#121212',
|
||||
'--dark-color': '#ffffff',
|
||||
|
||||
'--bg-primary': '#121212',
|
||||
'--bg-secondary': '#1e1e1e',
|
||||
'--bg-accent': '#2d2d2d',
|
||||
|
||||
'--text-primary': '#ffffff',
|
||||
'--text-secondary': '#b0b0b0',
|
||||
'--text-muted': '#8e8e8e',
|
||||
|
||||
'--border-color': '#3a3a3a',
|
||||
'--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.5)',
|
||||
'--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.3)',
|
||||
|
||||
'--gold-color': '#ffd43b',
|
||||
'--gold-dark': '#e0a800',
|
||||
'--purple-dark': '#6f42c1',
|
||||
'--purple-mid': '#7b1fa2',
|
||||
'--purple-light': '#ab47bc',
|
||||
|
||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'--font-size-base': '1rem',
|
||||
'--line-height-base': '1.5',
|
||||
'--border-radius': '0.375rem',
|
||||
'--border-radius-lg': '0.5rem',
|
||||
'--border-radius-sm': '0.25rem'
|
||||
}
|
||||
},
|
||||
blue: {
|
||||
name: 'Blue',
|
||||
description: 'Professional blue color scheme',
|
||||
cssVariables: {
|
||||
'--primary-color': '#1976d2',
|
||||
'--secondary-color': '#424242',
|
||||
'--success-color': '#2e7d32',
|
||||
'--danger-color': '#c62828',
|
||||
'--warning-color': '#ef6c00',
|
||||
'--info-color': '#0277bd',
|
||||
'--light-color': '#e3f2fd',
|
||||
'--dark-color': '#212121',
|
||||
|
||||
'--bg-primary': '#ffffff',
|
||||
'--bg-secondary': '#f5f5f5',
|
||||
'--bg-accent': '#e8eaf6',
|
||||
|
||||
'--text-primary': '#212121',
|
||||
'--text-secondary': '#424242',
|
||||
'--text-muted': '#757575',
|
||||
|
||||
'--border-color': '#e0e0e0',
|
||||
'--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
||||
'--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
|
||||
|
||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'--font-size-base': '1rem',
|
||||
'--line-height-base': '1.5',
|
||||
'--border-radius': '0.375rem',
|
||||
'--border-radius-lg': '0.5rem',
|
||||
'--border-radius-sm': '0.25rem'
|
||||
}
|
||||
},
|
||||
green: {
|
||||
name: 'Green',
|
||||
description: 'Fresh and modern green theme',
|
||||
cssVariables: {
|
||||
'--primary-color': '#2e7d32',
|
||||
'--secondary-color': '#5d4037',
|
||||
'--success-color': '#4caf50',
|
||||
'--danger-color': '#d32f2f',
|
||||
'--warning-color': '#f57c00',
|
||||
'--info-color': '#00838f',
|
||||
'--light-color': '#e8f5e9',
|
||||
'--dark-color': '#1b5e20',
|
||||
|
||||
'--bg-primary': '#ffffff',
|
||||
'--bg-secondary': '#f1f8e9',
|
||||
'--bg-accent': '#e8f5e9',
|
||||
|
||||
'--text-primary': '#1b5e20',
|
||||
'--text-secondary': '#4caf50',
|
||||
'--text-muted': '#757575',
|
||||
|
||||
'--border-color': '#c8e6c9',
|
||||
'--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
||||
'--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
|
||||
|
||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'--font-size-base': '1rem',
|
||||
'--line-height-base': '1.5',
|
||||
'--border-radius': '0.375rem',
|
||||
'--border-radius-lg': '0.5rem',
|
||||
'--border-radius-sm': '0.25rem'
|
||||
}
|
||||
},
|
||||
purple: {
|
||||
name: 'Purple',
|
||||
description: 'Elegant purple color scheme',
|
||||
cssVariables: {
|
||||
'--primary-color': '#7b1fa2',
|
||||
'--secondary-color': '#4527a0',
|
||||
'--success-color': '#6a1b9a',
|
||||
'--danger-color': '#d81b60',
|
||||
'--warning-color': '#ef6c00',
|
||||
'--info-color': '#00acc1',
|
||||
'--light-color': '#f3e5f5',
|
||||
'--dark-color': '#4a148c',
|
||||
|
||||
'--bg-primary': '#ffffff',
|
||||
'--bg-secondary': '#f3e5f5',
|
||||
'--bg-accent': '#e1bee7',
|
||||
|
||||
'--text-primary': '#4a148c',
|
||||
'--text-secondary': '#7b1fa2',
|
||||
'--text-muted': '#757575',
|
||||
|
||||
'--border-color': '#ce93d8',
|
||||
'--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
||||
'--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
|
||||
|
||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'--font-size-base': '1rem',
|
||||
'--line-height-base': '1.5',
|
||||
'--border-radius': '0.375rem',
|
||||
'--border-radius-lg': '0.5rem',
|
||||
'--border-radius-sm': '0.25rem'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.currentTheme = 'royal';
|
||||
this.element = document.documentElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set theme by name
|
||||
*/
|
||||
setTheme(themeName) {
|
||||
if (!this.themes[themeName]) {
|
||||
console.warn(`Theme '${themeName}' not found. Using default theme.`);
|
||||
themeName = 'light';
|
||||
}
|
||||
|
||||
this.currentTheme = themeName;
|
||||
this.applyTheme(themeName);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('app-theme', themeName);
|
||||
|
||||
// Dispatch theme change event
|
||||
this.dispatchThemeChange(themeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme
|
||||
*/
|
||||
getCurrentTheme() {
|
||||
return this.currentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme by name
|
||||
*/
|
||||
getTheme(themeName) {
|
||||
return this.themes[themeName] || this.themes['light'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available themes
|
||||
*/
|
||||
getThemes() {
|
||||
return { ...this.themes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme to document
|
||||
*/
|
||||
applyTheme(themeName) {
|
||||
const theme = this.themes[themeName];
|
||||
if (!theme) return;
|
||||
|
||||
// Remove existing theme classes
|
||||
Object.keys(this.themes).forEach(name => {
|
||||
this.element.classList.remove(`theme-${name}`);
|
||||
});
|
||||
|
||||
// Add current theme class
|
||||
this.element.classList.add(`theme-${themeName}`);
|
||||
|
||||
// Apply CSS custom properties
|
||||
const root = document.documentElement;
|
||||
Object.entries(theme.cssVariables).forEach(([property, value]) => {
|
||||
root.style.setProperty(property, value);
|
||||
});
|
||||
|
||||
// Update meta theme-color for mobile browsers
|
||||
this.updateMetaThemeColor(theme.cssVariables['--primary-color']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update meta theme-color for mobile browsers
|
||||
*/
|
||||
updateMetaThemeColor(color) {
|
||||
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
|
||||
if (metaThemeColor) {
|
||||
metaThemeColor.setAttribute('content', color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between Royal (light) and Royal Dark themes
|
||||
*/
|
||||
toggleTheme() {
|
||||
const current = this.getCurrentTheme();
|
||||
const newTheme = current === 'royal-dark' ? 'royal' : 'royal-dark';
|
||||
this.setTheme(newTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch theme change event
|
||||
*/
|
||||
dispatchThemeChange(themeName) {
|
||||
const event = new CustomEvent('theme:changed', {
|
||||
detail: {
|
||||
theme: themeName,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme from localStorage or system preference
|
||||
* Defaults to the Royal (Purple/Gold/Black/White) theme
|
||||
*/
|
||||
init() {
|
||||
// Check localStorage first
|
||||
const savedTheme = localStorage.getItem('app-theme');
|
||||
if (savedTheme && this.themes[savedTheme]) {
|
||||
this.setTheme(savedTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check system preference — map to Royal variants
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
this.setTheme('royal-dark');
|
||||
} else {
|
||||
this.setTheme('royal');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create theme switcher component
|
||||
*/
|
||||
createThemeSwitcher(container) {
|
||||
if (!container) return;
|
||||
|
||||
const switcher = document.createElement('div');
|
||||
switcher.className = 'theme-switcher';
|
||||
switcher.innerHTML = `
|
||||
<label class="theme-switcher-label">
|
||||
<span class="theme-switcher-text">Theme:</span>
|
||||
<select class="form-select theme-switcher-select" style="width: auto; display: inline-block;">
|
||||
${Object.entries(this.themes).map(([name, theme]) =>
|
||||
`<option value="${name}" ${name === this.currentTheme ? 'selected' : ''}>
|
||||
${theme.name}
|
||||
</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</label>
|
||||
`;
|
||||
|
||||
container.appendChild(switcher);
|
||||
|
||||
// Add event listener
|
||||
const select = switcher.querySelector('.theme-switcher-select');
|
||||
select.addEventListener('change', (e) => {
|
||||
this.setTheme(e.target.value);
|
||||
});
|
||||
|
||||
return switcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create theme preview component
|
||||
*/
|
||||
createThemePreview(container) {
|
||||
if (!container) return;
|
||||
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'theme-preview';
|
||||
preview.innerHTML = `
|
||||
<div class="row">
|
||||
${Object.entries(this.themes).map(([name, theme]) => `
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="theme-card" data-theme="${name}" style="cursor: pointer; border: 2px solid transparent; border-radius: 8px; padding: 15px; transition: all 0.3s ease;">
|
||||
<div style="height: 40px; background: ${theme.cssVariables['--bg-primary']}; border-radius: 4px; margin-bottom: 8px;"></div>
|
||||
<div style="height: 20px; background: ${theme.cssVariables['--primary-color']}; border-radius: 4px; margin-bottom: 8px;"></div>
|
||||
<div style="height: 15px; background: ${theme.cssVariables['--secondary-color']}; border-radius: 4px;"></div>
|
||||
<div style="font-size: 12px; color: ${theme.cssVariables['--text-secondary']}; margin-top: 8px;">
|
||||
${theme.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(preview);
|
||||
|
||||
// Add click listeners
|
||||
const themeCards = preview.querySelectorAll('.theme-card');
|
||||
themeCards.forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const themeName = card.getAttribute('data-theme');
|
||||
this.setTheme(themeName);
|
||||
|
||||
// Update selection indicators
|
||||
themeCards.forEach(c => c.style.borderColor = 'transparent');
|
||||
card.style.borderColor = this.themes[themeName].cssVariables['--primary-color'];
|
||||
});
|
||||
});
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export current theme
|
||||
*/
|
||||
exportTheme() {
|
||||
const theme = this.getTheme(this.currentTheme);
|
||||
return {
|
||||
name: theme.name,
|
||||
description: theme.description,
|
||||
cssVariables: { ...theme.cssVariables },
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import custom theme
|
||||
*/
|
||||
importTheme(themeData) {
|
||||
if (!themeData || !themeData.cssVariables) {
|
||||
throw new Error('Invalid theme data');
|
||||
}
|
||||
|
||||
const themeName = themeData.name || 'custom';
|
||||
this.themes[themeName] = {
|
||||
name: themeData.name || 'Custom Theme',
|
||||
description: themeData.description || 'Custom imported theme',
|
||||
cssVariables: { ...themeData.cssVariables }
|
||||
};
|
||||
|
||||
this.setTheme(themeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom theme from colors
|
||||
*/
|
||||
createCustomTheme(colors) {
|
||||
const customTheme = {
|
||||
name: 'Custom',
|
||||
description: 'User-defined theme',
|
||||
cssVariables: {
|
||||
'--primary-color': colors.primary || '#007bff',
|
||||
'--secondary-color': colors.secondary || '#6c757d',
|
||||
'--success-color': colors.success || '#28a745',
|
||||
'--danger-color': colors.danger || '#dc3545',
|
||||
'--warning-color': colors.warning || '#ffc107',
|
||||
'--info-color': colors.info || '#17a2b8',
|
||||
'--light-color': colors.light || '#f8f9fa',
|
||||
'--dark-color': colors.dark || '#343a40',
|
||||
|
||||
'--bg-primary': colors.bgPrimary || '#ffffff',
|
||||
'--bg-secondary': colors.bgSecondary || '#f8f9fa',
|
||||
'--bg-accent': colors.bgAccent || '#e9ecef',
|
||||
|
||||
'--text-primary': colors.textPrimary || '#212529',
|
||||
'--text-secondary': colors.textSecondary || '#6c757d',
|
||||
'--text-muted': colors.textMuted || '#6c757d',
|
||||
|
||||
'--border-color': colors.borderColor || '#dee2e6',
|
||||
'--shadow': '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
||||
'--shadow-lg': '0 0.5rem 1rem rgba(0, 0, 0, 0.15)',
|
||||
|
||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
'--font-size-base': '1rem',
|
||||
'--line-height-base': '1.5',
|
||||
'--border-radius': '0.375rem',
|
||||
'--border-radius-lg': '0.5rem',
|
||||
'--border-radius-sm': '0.25rem'
|
||||
}
|
||||
};
|
||||
|
||||
this.themes.custom = customTheme;
|
||||
this.setTheme('custom');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default theme
|
||||
*/
|
||||
resetTheme() {
|
||||
this.setTheme('light');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if theme exists
|
||||
*/
|
||||
hasTheme(themeName) {
|
||||
return !!this.themes[themeName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove theme
|
||||
*/
|
||||
removeTheme(themeName) {
|
||||
if (themeName === this.currentTheme) {
|
||||
this.setTheme('light');
|
||||
}
|
||||
delete this.themes[themeName];
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ThemeManager;
|
||||
} else {
|
||||
window.ThemeManager = ThemeManager;
|
||||
}
|
||||
|
|
@ -0,0 +1,729 @@
|
|||
/**
|
||||
* Event Handlers of Web Application
|
||||
*
|
||||
* Centralized event handling for the Excel Analyzer application.
|
||||
*/
|
||||
|
||||
class EventHandlers {
|
||||
constructor(options = {}) {
|
||||
this.appState = options.appState || {};
|
||||
this.modules = options.modules || {};
|
||||
this.utils = options.utils || {};
|
||||
this.config = options.config || {};
|
||||
this.events = options.events || {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupGlobalEventListeners();
|
||||
this.setupModuleEventListeners();
|
||||
this.setupUIEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup global event listeners
|
||||
*/
|
||||
setupGlobalEventListeners() {
|
||||
// Window resize events
|
||||
window.addEventListener('resize', this.handleWindowResize.bind(this));
|
||||
|
||||
// Keyboard shortcuts
|
||||
window.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
|
||||
// Before unload confirmation
|
||||
window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
|
||||
|
||||
// Theme change events
|
||||
window.addEventListener('theme:changed', this.handleThemeChange.bind(this));
|
||||
|
||||
// App initialization events
|
||||
this.events.on('app:initialized', this.handleAppInitialized.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup module-specific event listeners
|
||||
*/
|
||||
setupModuleEventListeners() {
|
||||
// File upload events
|
||||
if (this.modules.fileUpload) {
|
||||
this.modules.fileUpload.onFileSelected = this.handleFileSelected.bind(this);
|
||||
this.modules.fileUpload.onFileParsed = this.handleFileParsed.bind(this);
|
||||
this.modules.fileUpload.onError = this.handleModuleError.bind(this);
|
||||
}
|
||||
|
||||
// Data processing events
|
||||
if (this.modules.dataProcessor) {
|
||||
this.modules.dataProcessor.onDataAnalyzed = this.handleDataAnalyzed.bind(this);
|
||||
this.modules.dataProcessor.onDataProcessed = this.handleDataProcessed.bind(this);
|
||||
this.modules.dataProcessor.onError = this.handleModuleError.bind(this);
|
||||
}
|
||||
|
||||
// Visualization events
|
||||
if (this.modules.visualizer) {
|
||||
this.modules.visualizer.onChartRendered = this.handleChartRendered.bind(this);
|
||||
this.modules.visualizer.onChartUpdated = this.handleChartUpdated.bind(this);
|
||||
this.modules.visualizer.onError = this.handleModuleError.bind(this);
|
||||
}
|
||||
|
||||
// Dashboard events
|
||||
if (this.modules.dashboard) {
|
||||
this.modules.dashboard.onDashboardUpdated = this.handleDashboardUpdated.bind(this);
|
||||
this.modules.dashboard.onWidgetAdded = this.handleWidgetAdded.bind(this);
|
||||
this.modules.dashboard.onError = this.handleModuleError.bind(this);
|
||||
}
|
||||
|
||||
// Export events
|
||||
if (this.modules.exporter) {
|
||||
this.modules.exporter.onExportComplete = this.handleExportComplete.bind(this);
|
||||
this.modules.exporter.onExportProgress = this.handleExportProgress.bind(this);
|
||||
this.modules.exporter.onError = this.handleModuleError.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup UI event listeners
|
||||
*/
|
||||
setupUIEventListeners() {
|
||||
// Navigation events
|
||||
this.setupNavigationEvents();
|
||||
|
||||
// Chart selection events
|
||||
this.setupChartSelectionEvents();
|
||||
|
||||
// Dashboard layout events
|
||||
this.setupDashboardLayoutEvents();
|
||||
|
||||
// Export events
|
||||
this.setupExportEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle window resize
|
||||
*/
|
||||
handleWindowResize() {
|
||||
// Resize charts if visualizer is available
|
||||
if (this.modules.visualizer && this.modules.visualizer.resizeCharts) {
|
||||
this.modules.visualizer.resizeCharts();
|
||||
}
|
||||
|
||||
// Update dashboard layout if needed
|
||||
if (this.modules.dashboard && this.modules.dashboard.handleResize) {
|
||||
this.modules.dashboard.handleResize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keyboard shortcuts
|
||||
*/
|
||||
handleKeyDown(event) {
|
||||
// Ctrl/Cmd + S: Save dashboard
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
this.handleSaveDashboard();
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + E: Export
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
|
||||
event.preventDefault();
|
||||
this.handleExport();
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + T: Toggle theme
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 't') {
|
||||
event.preventDefault();
|
||||
this.handleThemeToggle();
|
||||
}
|
||||
|
||||
// Escape: Close modals
|
||||
if (event.key === 'Escape') {
|
||||
this.handleEscapeKey();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle before unload
|
||||
*/
|
||||
handleBeforeUnload(event) {
|
||||
if (this.appState.rawData && this.appState.rawData.length > 0) {
|
||||
event.preventDefault();
|
||||
event.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle theme change
|
||||
*/
|
||||
handleThemeChange(event) {
|
||||
const theme = event.detail.theme;
|
||||
console.log(`Theme changed to: ${theme}`);
|
||||
|
||||
// Update any theme-dependent components
|
||||
if (this.modules.visualizer) {
|
||||
this.modules.visualizer.updateTheme(theme);
|
||||
}
|
||||
|
||||
if (this.modules.dashboard) {
|
||||
this.modules.dashboard.updateTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle app initialization
|
||||
*/
|
||||
handleAppInitialized(event) {
|
||||
console.log('Application initialized successfully');
|
||||
|
||||
// Setup any post-initialization tasks
|
||||
this.setupPostInitialization();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file selected
|
||||
*/
|
||||
handleFileSelected(file) {
|
||||
console.log('File selected:', file.name);
|
||||
|
||||
// Update app state
|
||||
this.appState.currentFile = file;
|
||||
this.appState.rawData = [];
|
||||
this.appState.processedData = [];
|
||||
this.appState.dataStructure = null;
|
||||
this.appState.visualizations = [];
|
||||
|
||||
// Update UI
|
||||
this.updateFileInfo(file);
|
||||
this.showSection('preview');
|
||||
|
||||
// Save state
|
||||
this.saveAppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file parsed
|
||||
*/
|
||||
handleFileParsed(data) {
|
||||
console.log('File parsed successfully');
|
||||
|
||||
this.appState.rawData = data;
|
||||
|
||||
// Update UI
|
||||
this.updateDataPreview(data);
|
||||
|
||||
// Save state
|
||||
this.saveAppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data analyzed
|
||||
*/
|
||||
handleDataAnalyzed(structure) {
|
||||
console.log('Data analyzed');
|
||||
|
||||
this.appState.dataStructure = structure;
|
||||
|
||||
// Update UI
|
||||
this.updateColumnAnalysis(structure);
|
||||
this.showSection('analyze');
|
||||
|
||||
// Save state
|
||||
this.saveAppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle data processed
|
||||
*/
|
||||
handleDataProcessed(processedData) {
|
||||
console.log('Data processed');
|
||||
|
||||
this.appState.processedData = processedData;
|
||||
|
||||
// Save state
|
||||
this.saveAppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle chart rendered
|
||||
*/
|
||||
handleChartRendered(chart) {
|
||||
console.log('Chart rendered');
|
||||
|
||||
this.appState.visualizations.push(chart);
|
||||
|
||||
// Save state
|
||||
this.saveAppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle chart updated
|
||||
*/
|
||||
handleChartUpdated(chart) {
|
||||
console.log('Chart updated');
|
||||
|
||||
const index = this.appState.visualizations.findIndex(c => c.id === chart.id);
|
||||
if (index !== -1) {
|
||||
this.appState.visualizations[index] = chart;
|
||||
}
|
||||
|
||||
// Save state
|
||||
this.saveAppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dashboard updated
|
||||
*/
|
||||
handleDashboardUpdated(dashboard) {
|
||||
console.log('Dashboard updated');
|
||||
|
||||
this.appState.dashboard = dashboard;
|
||||
|
||||
// Save state
|
||||
this.saveAppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle widget added
|
||||
*/
|
||||
handleWidgetAdded(widget) {
|
||||
console.log('Widget added to dashboard');
|
||||
|
||||
// Dashboard manager handles this internally
|
||||
|
||||
// Save state
|
||||
this.saveAppState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle export complete
|
||||
*/
|
||||
handleExportComplete(result) {
|
||||
console.log('Export completed:', result);
|
||||
|
||||
this.showSuccessMessage(`Export completed successfully: ${result.filename}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle export progress
|
||||
*/
|
||||
handleExportProgress(progress) {
|
||||
console.log('Export progress:', progress);
|
||||
|
||||
this.updateExportProgress(progress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle module error
|
||||
*/
|
||||
handleModuleError(error) {
|
||||
console.error('Module error:', error);
|
||||
|
||||
this.showErrorMessage(error.message || 'An error occurred. Please try again.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup navigation events
|
||||
*/
|
||||
setupNavigationEvents() {
|
||||
const links = document.querySelectorAll('.nav-link[data-section]');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const sectionId = link.getAttribute('data-section');
|
||||
this.showSection(sectionId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup chart selection events
|
||||
*/
|
||||
setupChartSelectionEvents() {
|
||||
const chartTypeSelector = document.getElementById('chartTypeSelector');
|
||||
const xAxisSelector = document.getElementById('xAxisSelector');
|
||||
const yAxisSelector = document.getElementById('yAxisSelector');
|
||||
|
||||
if (chartTypeSelector) {
|
||||
chartTypeSelector.addEventListener('change', (e) => {
|
||||
this.handleChartTypeChange(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (xAxisSelector) {
|
||||
xAxisSelector.addEventListener('change', (e) => {
|
||||
this.handleAxisChange('x', e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
if (yAxisSelector) {
|
||||
yAxisSelector.addEventListener('change', (e) => {
|
||||
this.handleAxisChange('y', e.target.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup dashboard layout events
|
||||
*/
|
||||
setupDashboardLayoutEvents() {
|
||||
const layoutButtons = document.querySelectorAll('.btn[data-layout]');
|
||||
layoutButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const layout = e.target.getAttribute('data-layout');
|
||||
this.handleLayoutChange(layout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup export events
|
||||
*/
|
||||
setupExportEvents() {
|
||||
const exportButtons = document.querySelectorAll('[data-export-format]');
|
||||
exportButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
const format = e.target.getAttribute('data-export-format');
|
||||
this.handleExport(format);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle chart type change
|
||||
*/
|
||||
handleChartTypeChange(chartType) {
|
||||
console.log('Chart type changed to:', chartType);
|
||||
|
||||
// Update chart configuration
|
||||
if (this.modules.visualizer) {
|
||||
this.modules.visualizer.updateChartType(chartType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle axis change
|
||||
*/
|
||||
handleAxisChange(axis, value) {
|
||||
console.log(`${axis} axis changed to:`, value);
|
||||
|
||||
// Update chart configuration
|
||||
if (this.modules.visualizer) {
|
||||
this.modules.visualizer.updateAxis(axis, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle layout change
|
||||
*/
|
||||
handleLayoutChange(layout) {
|
||||
console.log('Layout changed to:', layout);
|
||||
|
||||
// Update dashboard layout
|
||||
if (this.modules.dashboard) {
|
||||
this.modules.dashboard.setLayout(layout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle export
|
||||
*/
|
||||
handleExport(format = 'pdf') {
|
||||
console.log('Export requested:', format);
|
||||
|
||||
if (this.modules.exporter) {
|
||||
this.modules.exporter.export(format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save dashboard
|
||||
*/
|
||||
handleSaveDashboard() {
|
||||
console.log('Save dashboard requested');
|
||||
|
||||
if (this.modules.dashboard) {
|
||||
this.modules.dashboard.save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle theme toggle
|
||||
*/
|
||||
handleThemeToggle() {
|
||||
console.log('Theme toggle requested');
|
||||
|
||||
if (this.config.themes) {
|
||||
this.config.themes.toggleTheme();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle escape key
|
||||
*/
|
||||
handleEscapeKey() {
|
||||
console.log('Escape key pressed');
|
||||
|
||||
// Close any open modals or dropdowns
|
||||
const modals = document.querySelectorAll('.modal.show');
|
||||
modals.forEach(modal => {
|
||||
const modalInstance = bootstrap.Modal.getInstance(modal);
|
||||
if (modalInstance) {
|
||||
modalInstance.hide();
|
||||
}
|
||||
});
|
||||
|
||||
const dropdowns = document.querySelectorAll('.dropdown.show');
|
||||
dropdowns.forEach(dropdown => {
|
||||
const dropdownInstance = bootstrap.Dropdown.getInstance(dropdown);
|
||||
if (dropdownInstance) {
|
||||
dropdownInstance.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup post-initialization tasks
|
||||
*/
|
||||
setupPostInitialization() {
|
||||
// Load saved state if available
|
||||
this.loadAppState();
|
||||
|
||||
// Initialize tooltips
|
||||
this.initializeTooltips();
|
||||
|
||||
// Setup theme switcher if available
|
||||
this.setupThemeSwitcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update file info display
|
||||
*/
|
||||
updateFileInfo(file) {
|
||||
const fileNameEl = document.getElementById('fileName');
|
||||
const fileSizeEl = document.getElementById('fileSize');
|
||||
|
||||
if (fileNameEl) fileNameEl.textContent = file.name;
|
||||
if (fileSizeEl) fileSizeEl.textContent = this.utils.formatters.formatFileSize(file.size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data preview
|
||||
*/
|
||||
updateDataPreview(data) {
|
||||
// Update statistics
|
||||
const totalRowsEl = document.getElementById('totalRows');
|
||||
const totalColsEl = document.getElementById('totalCols');
|
||||
const missingValuesEl = document.getElementById('missingValues');
|
||||
const dataQualityEl = document.getElementById('dataQuality');
|
||||
|
||||
if (totalRowsEl) totalRowsEl.textContent = data.length;
|
||||
if (totalColsEl && data.length > 0) totalColsEl.textContent = Object.keys(data[0]).length;
|
||||
|
||||
// Calculate missing values and data quality
|
||||
const stats = this.calculateDataStats(data);
|
||||
if (missingValuesEl) missingValuesEl.textContent = stats.missingCount;
|
||||
if (dataQualityEl) dataQualityEl.textContent = `${stats.qualityPercentage}%`;
|
||||
|
||||
// Update table
|
||||
this.renderDataTable(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update column analysis
|
||||
*/
|
||||
updateColumnAnalysis(structure) {
|
||||
const container = document.getElementById('columnAnalysis');
|
||||
if (container && structure) {
|
||||
container.innerHTML = this.generateColumnAnalysisHTML(structure);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show section
|
||||
*/
|
||||
showSection(sectionId) {
|
||||
const sections = ['upload', 'preview', 'analyze', 'dashboard', 'export'];
|
||||
sections.forEach(section => {
|
||||
const element = document.getElementById(section);
|
||||
if (element) {
|
||||
element.style.display = section === sectionId ? 'block' : 'none';
|
||||
if (section === sectionId) {
|
||||
element.classList.add('fade-in');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update navigation
|
||||
this.updateNavigationActive(sectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update navigation active state
|
||||
*/
|
||||
updateNavigationActive(sectionId) {
|
||||
const links = document.querySelectorAll('.nav-link');
|
||||
links.forEach(link => {
|
||||
if (link.getAttribute('data-section') === sectionId) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show success message
|
||||
*/
|
||||
showSuccessMessage(message) {
|
||||
const statusEl = document.getElementById('exportStatus');
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = `<div class="alert alert-success">${message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
showErrorMessage(message) {
|
||||
const statusEl = document.getElementById('uploadStatus') || document.getElementById('exportStatus');
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = `<div class="alert alert-danger">${message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update export progress
|
||||
*/
|
||||
updateExportProgress(progress) {
|
||||
const progressEl = document.getElementById('exportProgress');
|
||||
const progressBarEl = progressEl ? progressEl.querySelector('.progress-bar') : null;
|
||||
|
||||
if (progressEl) {
|
||||
progressEl.style.display = 'block';
|
||||
}
|
||||
|
||||
if (progressBarEl) {
|
||||
progressBarEl.style.width = `${progress.percentage}%`;
|
||||
progressBarEl.textContent = `${progress.percentage}%`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render data table
|
||||
*/
|
||||
renderDataTable(data) {
|
||||
const tableBody = document.getElementById('tableBody');
|
||||
const tableHeader = document.getElementById('tableHeader');
|
||||
|
||||
if (!tableBody || !tableHeader || data.length === 0) return;
|
||||
|
||||
// Generate headers
|
||||
const headers = Object.keys(data[0]);
|
||||
tableHeader.innerHTML = '<tr>' + headers.map(header => `<th>${header}</th>`).join('') + '</tr>';
|
||||
|
||||
// Generate rows (limit to first 100 for performance)
|
||||
const displayData = data.slice(0, 100);
|
||||
tableBody.innerHTML = displayData.map(row => {
|
||||
return '<tr>' + headers.map(header => `<td>${row[header] || ''}</td>`).join('') + '</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate data statistics
|
||||
*/
|
||||
calculateDataStats(data) {
|
||||
if (data.length === 0) return { missingCount: 0, qualityPercentage: 100 };
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
let missingCount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
data.forEach(row => {
|
||||
headers.forEach(header => {
|
||||
totalCount++;
|
||||
if (row[header] === null || row[header] === undefined || row[header] === '') {
|
||||
missingCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const qualityPercentage = Math.round(((totalCount - missingCount) / totalCount) * 100);
|
||||
|
||||
return {
|
||||
missingCount,
|
||||
qualityPercentage
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate column analysis HTML
|
||||
*/
|
||||
generateColumnAnalysisHTML(structure) {
|
||||
if (!structure || !structure.columns) return '';
|
||||
|
||||
return structure.columns.map(col => `
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">${col.name}</h6>
|
||||
<p class="card-text">
|
||||
<small class="text-muted">Type: ${col.type}</small><br>
|
||||
<small class="text-muted">Unique Values: ${col.uniqueValues}</small><br>
|
||||
<small class="text-muted">Missing: ${col.missingCount}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize tooltips
|
||||
*/
|
||||
initializeTooltips() {
|
||||
if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup theme switcher
|
||||
*/
|
||||
setupThemeSwitcher() {
|
||||
const themeContainer = document.getElementById('theme-switcher-container');
|
||||
if (themeContainer && this.config.themes) {
|
||||
this.config.themes.createThemeSwitcher(themeContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save application state
|
||||
*/
|
||||
saveAppState() {
|
||||
localStorage.setItem('excel-analyzer-state', JSON.stringify(this.appState));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load application state
|
||||
*/
|
||||
loadAppState() {
|
||||
const savedState = localStorage.getItem('excel-analyzer-state');
|
||||
if (savedState) {
|
||||
try {
|
||||
const state = JSON.parse(savedState);
|
||||
Object.assign(this.appState, state);
|
||||
console.log('Application state loaded from localStorage');
|
||||
} catch (error) {
|
||||
console.warn('Failed to load saved state:', error);
|
||||
localStorage.removeItem('excel-analyzer-state');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = EventHandlers;
|
||||
} else {
|
||||
window.EventHandlers = EventHandlers;
|
||||
}
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
/**
|
||||
* Event System of Web Application
|
||||
*
|
||||
* Custom event management system for the Excel Analyzer application.
|
||||
*/
|
||||
|
||||
class EventSystem {
|
||||
constructor() {
|
||||
this.events = new Map();
|
||||
this.maxListeners = 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listener
|
||||
*/
|
||||
on(event, callback, context = null) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('Callback must be a function');
|
||||
}
|
||||
|
||||
if (!this.events.has(event)) {
|
||||
this.events.set(event, new Set());
|
||||
}
|
||||
|
||||
const listeners = this.events.get(event);
|
||||
|
||||
// Check max listeners limit
|
||||
if (listeners.size >= this.maxListeners) {
|
||||
console.warn(`Maximum number of listeners (${this.maxListeners}) reached for event: ${event}`);
|
||||
}
|
||||
|
||||
listeners.add({ callback, context });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one-time event listener
|
||||
*/
|
||||
once(event, callback, context = null) {
|
||||
const onceCallback = (...args) => {
|
||||
this.off(event, onceCallback);
|
||||
callback.apply(context, args);
|
||||
};
|
||||
|
||||
return this.on(event, onceCallback, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove event listener
|
||||
*/
|
||||
off(event, callback) {
|
||||
if (!this.events.has(event)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const listeners = this.events.get(event);
|
||||
|
||||
if (callback) {
|
||||
// Remove specific callback
|
||||
for (const listener of listeners) {
|
||||
if (listener.callback === callback) {
|
||||
listeners.delete(listener);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove all listeners for this event
|
||||
listeners.clear();
|
||||
}
|
||||
|
||||
// Clean up empty event sets
|
||||
if (listeners.size === 0) {
|
||||
this.events.delete(event);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit event
|
||||
*/
|
||||
emit(event, data = null) {
|
||||
if (!this.events.has(event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const listeners = this.events.get(event);
|
||||
let hasListeners = false;
|
||||
|
||||
for (const listener of listeners) {
|
||||
hasListeners = true;
|
||||
try {
|
||||
listener.callback.call(listener.context, data);
|
||||
} catch (error) {
|
||||
console.error(`Error in event listener for '${event}':`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return hasListeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all listeners for an event
|
||||
*/
|
||||
listeners(event) {
|
||||
if (!this.events.has(event)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(this.events.get(event)).map(listener => listener.callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event has listeners
|
||||
*/
|
||||
hasListeners(event) {
|
||||
return this.events.has(event) && this.events.get(event).size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all listeners for all events
|
||||
*/
|
||||
removeAllListeners() {
|
||||
this.events.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all listeners for specific event
|
||||
*/
|
||||
removeAllListenersForEvent(event) {
|
||||
if (this.events.has(event)) {
|
||||
this.events.get(event).clear();
|
||||
this.events.delete(event);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set max listeners limit
|
||||
*/
|
||||
setMaxListeners(max) {
|
||||
if (typeof max !== 'number' || max < 0) {
|
||||
throw new Error('Max listeners must be a non-negative number');
|
||||
}
|
||||
|
||||
this.maxListeners = max;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event names
|
||||
*/
|
||||
eventNames() {
|
||||
return Array.from(this.events.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get listener count for event
|
||||
*/
|
||||
listenerCount(event) {
|
||||
return this.events.has(event) ? this.events.get(event).size : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create namespaced event
|
||||
*/
|
||||
namespace(namespace) {
|
||||
return {
|
||||
on: (event, callback, context) => this.on(`${namespace}:${event}`, callback, context),
|
||||
once: (event, callback, context) => this.once(`${namespace}:${event}`, callback, context),
|
||||
off: (event, callback) => this.off(`${namespace}:${event}`, callback),
|
||||
emit: (event, data) => this.emit(`${namespace}:${event}`, data),
|
||||
removeAllListeners: () => {
|
||||
const eventsToRemove = [];
|
||||
for (const eventName of this.eventNames()) {
|
||||
if (eventName.startsWith(`${namespace}:`)) {
|
||||
eventsToRemove.push(eventName);
|
||||
}
|
||||
}
|
||||
eventsToRemove.forEach(event => this.removeAllListenersForEvent(event));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create event chain
|
||||
*/
|
||||
chain(events) {
|
||||
if (!Array.isArray(events) || events.length < 2) {
|
||||
throw new Error('Event chain requires at least 2 events');
|
||||
}
|
||||
|
||||
return {
|
||||
start: (initialData) => {
|
||||
this.emit(events[0], initialData);
|
||||
},
|
||||
link: (event, callback) => {
|
||||
this.on(event, callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounced event emitter
|
||||
*/
|
||||
debouncedEmit(event, data, delay = 300) {
|
||||
if (!this._debounceTimers) {
|
||||
this._debounceTimers = new Map();
|
||||
}
|
||||
|
||||
if (this._debounceTimers.has(event)) {
|
||||
clearTimeout(this._debounceTimers.get(event));
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
this.emit(event, data);
|
||||
this._debounceTimers.delete(event);
|
||||
}, delay);
|
||||
|
||||
this._debounceTimers.set(event, timer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttled event emitter
|
||||
*/
|
||||
throttledEmit(event, data, limit = 1000) {
|
||||
if (!this._throttleTimers) {
|
||||
this._throttleTimers = new Map();
|
||||
}
|
||||
|
||||
const lastEmit = this._throttleTimers.get(event);
|
||||
const now = Date.now();
|
||||
|
||||
if (!lastEmit || (now - lastEmit) >= limit) {
|
||||
this.emit(event, data);
|
||||
this._throttleTimers.set(event, now);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event logger
|
||||
*/
|
||||
enableLogging(enabled = true) {
|
||||
if (enabled) {
|
||||
this._originalEmit = this.emit;
|
||||
this.emit = (event, data) => {
|
||||
console.log(`Event emitted: ${event}`, data);
|
||||
return this._originalEmit.call(this, event, data);
|
||||
};
|
||||
} else if (this._originalEmit) {
|
||||
this.emit = this._originalEmit;
|
||||
delete this._originalEmit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event history
|
||||
*/
|
||||
enableHistory(enabled = true, maxSize = 100) {
|
||||
if (enabled) {
|
||||
this._eventHistory = [];
|
||||
this._historyMaxSize = maxSize;
|
||||
|
||||
this._originalEmit = this.emit;
|
||||
this.emit = (event, data) => {
|
||||
const result = this._originalEmit.call(this, event, data);
|
||||
|
||||
// Add to history
|
||||
this._eventHistory.push({
|
||||
event,
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Limit history size
|
||||
if (this._eventHistory.length > this._historyMaxSize) {
|
||||
this._eventHistory.shift();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
} else if (this._originalEmit) {
|
||||
this.emit = this._originalEmit;
|
||||
delete this._originalEmit;
|
||||
delete this._eventHistory;
|
||||
delete this._historyMaxSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event history
|
||||
*/
|
||||
getHistory() {
|
||||
return this._eventHistory || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear event history
|
||||
*/
|
||||
clearHistory() {
|
||||
if (this._eventHistory) {
|
||||
this._eventHistory = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event filter
|
||||
*/
|
||||
filter(event, filterFn) {
|
||||
if (typeof filterFn !== 'function') {
|
||||
throw new Error('Filter function must be provided');
|
||||
}
|
||||
|
||||
const originalEmit = this.emit;
|
||||
this.emit = (eventName, data) => {
|
||||
if (eventName === event && !filterFn(data)) {
|
||||
return false; // Don't emit if filter returns false
|
||||
}
|
||||
return originalEmit.call(this, eventName, data);
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event transformer
|
||||
*/
|
||||
transform(event, transformFn) {
|
||||
if (typeof transformFn !== 'function') {
|
||||
throw new Error('Transform function must be provided');
|
||||
}
|
||||
|
||||
const originalEmit = this.emit;
|
||||
this.emit = (eventName, data) => {
|
||||
if (eventName === event) {
|
||||
data = transformFn(data);
|
||||
}
|
||||
return originalEmit.call(this, eventName, data);
|
||||
};
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = EventSystem;
|
||||
} else {
|
||||
window.EventSystem = EventSystem;
|
||||
}
|
||||
467
apps/web-roo-code/src/components/excel-analyzer/src/js/main.js
Normal file
467
apps/web-roo-code/src/components/excel-analyzer/src/js/main.js
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
/**
|
||||
* Excel Analyzer - Main Application Entry Point
|
||||
*
|
||||
* Provides an end-to-end browser workflow:
|
||||
* upload -> preview -> analysis/chart -> dashboard -> export in PDF, Excel or chart.
|
||||
*/
|
||||
|
||||
const appState = {
|
||||
currentFile: null,
|
||||
rawData: [],
|
||||
processedData: [],
|
||||
dataStructure: null,
|
||||
currentChart: null,
|
||||
previewLimit: 100
|
||||
};
|
||||
|
||||
const modules = {
|
||||
fileUpload: null,
|
||||
dataProcessor: null,
|
||||
visualizer: null,
|
||||
dashboard: null,
|
||||
exporter: null,
|
||||
notifications: null
|
||||
};
|
||||
|
||||
const config = {
|
||||
settings: null,
|
||||
themes: null,
|
||||
defaults: null
|
||||
};
|
||||
|
||||
function init() {
|
||||
try {
|
||||
initConfig();
|
||||
initModules();
|
||||
initUI();
|
||||
showInlineStatus('uploadStatus', 'success', 'Application ready. Upload an Excel file to begin.');
|
||||
} catch (error) {
|
||||
console.error('Initialization failed:', error);
|
||||
showInlineStatus('uploadStatus', 'danger', `Initialization failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function initConfig() {
|
||||
config.settings = window.SettingsManager ? new SettingsManager() : null;
|
||||
config.themes = window.ThemeManager ? new ThemeManager() : null;
|
||||
config.defaults = window.DefaultValues ? new DefaultValues() : null;
|
||||
|
||||
const savedTheme =
|
||||
(config.settings && config.settings.get('ui.theme')) ||
|
||||
localStorage.getItem('app-theme') ||
|
||||
'light';
|
||||
|
||||
if (config.themes) {
|
||||
config.themes.setTheme(savedTheme);
|
||||
}
|
||||
}
|
||||
|
||||
function initModules() {
|
||||
modules.notifications = window.Notifications ? new Notifications() : null;
|
||||
|
||||
modules.fileUpload = new FileUploadManager({
|
||||
onFileSelected: handleFileSelected,
|
||||
onFileParsed: handleFileParsed,
|
||||
onError: handleModuleError
|
||||
});
|
||||
|
||||
modules.dataProcessor = new DataProcessor({ onError: handleModuleError });
|
||||
|
||||
modules.visualizer = new Visualizer({
|
||||
onChartRendered: (chart) => {
|
||||
appState.currentChart = chart;
|
||||
updateDashboard();
|
||||
},
|
||||
onError: handleModuleError
|
||||
});
|
||||
|
||||
modules.dashboard = new DashboardManager({
|
||||
containerId: 'dashboardGrid',
|
||||
onDashboardUpdated: () => {}
|
||||
});
|
||||
|
||||
modules.exporter = new Exporter({
|
||||
onExportProgress: (progress) => updateExportProgress(progress),
|
||||
onExportComplete: ({ filename }) => {
|
||||
showInlineStatus('exportStatus', 'success', `Export completed: ${filename}`);
|
||||
notify('success', `Export completed: ${filename}`);
|
||||
},
|
||||
onError: (error) => {
|
||||
showInlineStatus('exportStatus', 'danger', error.message || 'Export failed');
|
||||
notify('error', error.message || 'Export failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initUI() {
|
||||
hideAllSections();
|
||||
showSection('upload');
|
||||
|
||||
setupNavigation();
|
||||
setupPreviewControls();
|
||||
setupAnalysisControls();
|
||||
setupDashboardControls();
|
||||
setupExportControls();
|
||||
setupThemeToggle();
|
||||
}
|
||||
|
||||
function setupNavigation() {
|
||||
document.querySelectorAll('.nav-link[data-section]').forEach((link) => {
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const sectionId = link.getAttribute('data-section');
|
||||
if (sectionId !== 'upload' && !hasData()) {
|
||||
showInlineStatus('uploadStatus', 'warning', 'Please upload and parse a file first.');
|
||||
return;
|
||||
}
|
||||
showSection(sectionId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupPreviewControls() {
|
||||
const btnFirst100 = document.getElementById('btnFirst100');
|
||||
const btnAllData = document.getElementById('btnAllData');
|
||||
|
||||
if (btnFirst100) {
|
||||
btnFirst100.addEventListener('click', () => {
|
||||
appState.previewLimit = 100;
|
||||
renderDataTable(appState.processedData, appState.previewLimit);
|
||||
});
|
||||
}
|
||||
|
||||
if (btnAllData) {
|
||||
btnAllData.addEventListener('click', () => {
|
||||
appState.previewLimit = Number.MAX_SAFE_INTEGER;
|
||||
renderDataTable(appState.processedData, appState.previewLimit);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupAnalysisControls() {
|
||||
const chartTypeSelector = document.getElementById('chartTypeSelector');
|
||||
const xAxisSelector = document.getElementById('xAxisSelector');
|
||||
const yAxisSelector = document.getElementById('yAxisSelector');
|
||||
|
||||
if (chartTypeSelector) chartTypeSelector.addEventListener('change', renderSelectedChart);
|
||||
if (xAxisSelector) xAxisSelector.addEventListener('change', renderSelectedChart);
|
||||
if (yAxisSelector) yAxisSelector.addEventListener('change', renderSelectedChart);
|
||||
}
|
||||
|
||||
function setupDashboardControls() {
|
||||
const layouts = {
|
||||
layout1: 'layout1',
|
||||
layout2: 'layout2',
|
||||
layout3: 'layout3'
|
||||
};
|
||||
|
||||
Object.keys(layouts).forEach((id) => {
|
||||
const button = document.getElementById(id);
|
||||
if (!button) return;
|
||||
button.addEventListener('click', () => {
|
||||
modules.dashboard.setLayout(layouts[id]);
|
||||
updateDashboard();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupExportControls() {
|
||||
const exportPDF = document.getElementById('exportPDF');
|
||||
const exportExcel = document.getElementById('exportExcel');
|
||||
const exportCharts = document.getElementById('exportCharts');
|
||||
|
||||
if (exportPDF) exportPDF.addEventListener('click', () => exportData('pdf'));
|
||||
if (exportExcel) exportExcel.addEventListener('click', () => exportData('excel'));
|
||||
if (exportCharts) exportCharts.addEventListener('click', () => exportData('png'));
|
||||
}
|
||||
|
||||
function setupThemeToggle() {
|
||||
const toggle = document.getElementById('themeToggle');
|
||||
if (!toggle || !config.themes) return;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const current = config.themes.getCurrentTheme();
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
config.themes.setTheme(next);
|
||||
if (config.settings) {
|
||||
config.settings.set('ui.theme', next);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileSelected(file) {
|
||||
appState.currentFile = file;
|
||||
appState.rawData = [];
|
||||
appState.processedData = [];
|
||||
appState.dataStructure = null;
|
||||
appState.currentChart = null;
|
||||
|
||||
updateFileInfo(file);
|
||||
showInlineStatus('uploadStatus', 'info', `Selected file: ${file.name}`);
|
||||
}
|
||||
|
||||
function handleFileParsed(data) {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
showInlineStatus('uploadStatus', 'warning', 'File parsed, but no usable rows found.');
|
||||
return;
|
||||
}
|
||||
|
||||
appState.rawData = data;
|
||||
appState.processedData = data;
|
||||
appState.dataStructure = modules.dataProcessor.analyzeDataStructure(data);
|
||||
|
||||
updateDataPreview(appState.processedData);
|
||||
updateColumnAnalysis(appState.dataStructure);
|
||||
populateAxisSelectors(appState.processedData);
|
||||
renderSelectedChart();
|
||||
updateDashboard();
|
||||
|
||||
showSection('preview');
|
||||
notify('success', `Parsed successfully: ${data.length} rows.`);
|
||||
}
|
||||
|
||||
function handleModuleError(error) {
|
||||
console.error(error);
|
||||
const message = error?.message || 'An unexpected error occurred.';
|
||||
showInlineStatus('uploadStatus', 'danger', message);
|
||||
notify('error', message);
|
||||
}
|
||||
|
||||
function hasData() {
|
||||
return Array.isArray(appState.processedData) && appState.processedData.length > 0;
|
||||
}
|
||||
|
||||
function hideAllSections() {
|
||||
['upload', 'preview', 'analyze', 'dashboard', 'export'].forEach((id) => {
|
||||
const section = document.getElementById(id);
|
||||
if (section) section.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function showSection(sectionId) {
|
||||
hideAllSections();
|
||||
const section = document.getElementById(sectionId);
|
||||
if (section) {
|
||||
section.style.display = 'block';
|
||||
section.classList.add('fade-in');
|
||||
}
|
||||
|
||||
document.querySelectorAll('.nav-link[data-section]').forEach((link) => {
|
||||
link.classList.toggle('active', link.getAttribute('data-section') === sectionId);
|
||||
});
|
||||
}
|
||||
|
||||
function updateFileInfo(file) {
|
||||
const info = document.getElementById('fileInfo');
|
||||
const fileName = document.getElementById('fileName');
|
||||
const fileSize = document.getElementById('fileSize');
|
||||
|
||||
if (info) info.style.display = 'block';
|
||||
if (fileName) fileName.textContent = file.name;
|
||||
if (fileSize) fileSize.textContent = formatFileSize(file.size);
|
||||
}
|
||||
|
||||
function updateDataPreview(data) {
|
||||
const stats = calculateDataStats(data);
|
||||
|
||||
const totalRows = document.getElementById('totalRows');
|
||||
const totalCols = document.getElementById('totalCols');
|
||||
const missingValues = document.getElementById('missingValues');
|
||||
const dataQuality = document.getElementById('dataQuality');
|
||||
|
||||
if (totalRows) totalRows.textContent = data.length;
|
||||
if (totalCols) totalCols.textContent = data.length ? Object.keys(data[0]).length : 0;
|
||||
if (missingValues) missingValues.textContent = stats.missingCount;
|
||||
if (dataQuality) dataQuality.textContent = `${stats.qualityPercentage}%`;
|
||||
|
||||
renderDataTable(data, appState.previewLimit);
|
||||
}
|
||||
|
||||
function renderDataTable(data, limit = 100) {
|
||||
const tableHeader = document.getElementById('tableHeader');
|
||||
const tableBody = document.getElementById('tableBody');
|
||||
if (!tableHeader || !tableBody) return;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
tableHeader.innerHTML = '';
|
||||
tableBody.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
tableHeader.innerHTML = `<tr>${headers.map((h) => `<th>${h}</th>`).join('')}</tr>`;
|
||||
|
||||
const displayRows = data.slice(0, Math.min(limit, data.length));
|
||||
tableBody.innerHTML = displayRows
|
||||
.map((row) => {
|
||||
const cells = headers
|
||||
.map((header) => {
|
||||
const value = row[header];
|
||||
const missing = value === null || value === undefined || value === '';
|
||||
return `<td class="${missing ? 'table-warning' : ''}">${missing ? '' : String(value)}</td>`;
|
||||
})
|
||||
.join('');
|
||||
return `<tr>${cells}</tr>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function calculateDataStats(data) {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return { missingCount: 0, qualityPercentage: 100 };
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
let missingCount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
data.forEach((row) => {
|
||||
headers.forEach((header) => {
|
||||
totalCount += 1;
|
||||
const value = row[header];
|
||||
if (value === null || value === undefined || value === '') {
|
||||
missingCount += 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const qualityPercentage = totalCount ? Math.round(((totalCount - missingCount) / totalCount) * 100) : 100;
|
||||
return { missingCount, qualityPercentage };
|
||||
}
|
||||
|
||||
function updateColumnAnalysis(structure) {
|
||||
const container = document.getElementById('columnAnalysis');
|
||||
if (!container || !structure || !Array.isArray(structure.columns)) return;
|
||||
|
||||
container.innerHTML = structure.columns
|
||||
.map((col) => {
|
||||
const missing = col.missingCount ?? col.nullCount ?? 0;
|
||||
return `
|
||||
<div class="card mb-2">
|
||||
<div class="card-body p-2">
|
||||
<h6 class="mb-1">${col.name}</h6>
|
||||
<small class="text-muted">Type: ${col.type}</small><br>
|
||||
<small class="text-muted">Unique: ${col.uniqueValues ?? 0}</small><br>
|
||||
<small class="text-muted">Missing: ${missing}</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function populateAxisSelectors(data) {
|
||||
const xAxisSelector = document.getElementById('xAxisSelector');
|
||||
const yAxisSelector = document.getElementById('yAxisSelector');
|
||||
if (!xAxisSelector || !yAxisSelector || !data || !data.length) return;
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const numericHeaders = headers.filter((header) =>
|
||||
data.every((row) => row[header] === null || row[header] === '' || !Number.isNaN(Number(row[header])))
|
||||
);
|
||||
|
||||
xAxisSelector.innerHTML = headers.map((header) => `<option value="${header}">${header}</option>`).join('');
|
||||
yAxisSelector.innerHTML = headers.map((header) => `<option value="${header}">${header}</option>`).join('');
|
||||
|
||||
xAxisSelector.value = headers[0];
|
||||
yAxisSelector.value = numericHeaders[0] || headers[Math.min(1, headers.length - 1)] || headers[0];
|
||||
}
|
||||
|
||||
async function renderSelectedChart() {
|
||||
if (!hasData()) return;
|
||||
|
||||
const chartTypeSelector = document.getElementById('chartTypeSelector');
|
||||
const xAxisSelector = document.getElementById('xAxisSelector');
|
||||
const yAxisSelector = document.getElementById('yAxisSelector');
|
||||
|
||||
const type = chartTypeSelector ? chartTypeSelector.value : 'auto';
|
||||
const xAxis = xAxisSelector ? xAxisSelector.value : undefined;
|
||||
const yAxis = yAxisSelector ? yAxisSelector.value : undefined;
|
||||
|
||||
try {
|
||||
const chart = await modules.visualizer.renderChart(appState.processedData.slice(0, 1000), {
|
||||
type,
|
||||
xAxis,
|
||||
yAxis
|
||||
});
|
||||
appState.currentChart = chart;
|
||||
updateDashboard();
|
||||
} catch (error) {
|
||||
handleModuleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateDashboard() {
|
||||
if (!modules.dashboard) return;
|
||||
|
||||
const chartDataUrl =
|
||||
appState.currentChart && typeof appState.currentChart.toBase64Image === 'function'
|
||||
? appState.currentChart.toBase64Image('image/png', 1)
|
||||
: null;
|
||||
|
||||
modules.dashboard.render({
|
||||
data: appState.processedData,
|
||||
structure: appState.dataStructure,
|
||||
chartDataUrl
|
||||
});
|
||||
}
|
||||
|
||||
async function exportData(format) {
|
||||
if (!hasData()) {
|
||||
showInlineStatus('exportStatus', 'warning', 'No data available to export.');
|
||||
return;
|
||||
}
|
||||
|
||||
const baseName = (appState.currentFile?.name || 'excel-analyzer-data').replace(/\.[^.]+$/, '');
|
||||
|
||||
showInlineStatus('exportStatus', 'info', `Preparing ${format.toUpperCase()} export...`);
|
||||
|
||||
await modules.exporter.export(format, {
|
||||
filenameBase: baseName,
|
||||
title: `Excel Analyzer Report - ${baseName}`,
|
||||
rawData: appState.rawData,
|
||||
processedData: appState.processedData,
|
||||
dataStructure: appState.dataStructure,
|
||||
currentChart: appState.currentChart
|
||||
});
|
||||
}
|
||||
|
||||
function updateExportProgress(progress) {
|
||||
const wrapper = document.getElementById('exportProgress');
|
||||
const bar = wrapper ? wrapper.querySelector('.progress-bar') : null;
|
||||
if (!wrapper || !bar) return;
|
||||
|
||||
wrapper.style.display = 'block';
|
||||
const value = Math.max(0, Math.min(100, Number(progress?.percentage || 0)));
|
||||
bar.style.width = `${value}%`;
|
||||
bar.textContent = `${value}%`;
|
||||
}
|
||||
|
||||
function showInlineStatus(elementId, type, message) {
|
||||
const container = document.getElementById(elementId);
|
||||
if (!container) return;
|
||||
container.innerHTML = `<div class="alert alert-${type} mb-0">${message}</div>`;
|
||||
}
|
||||
|
||||
function notify(level, message) {
|
||||
if (!modules.notifications) return;
|
||||
const fn = modules.notifications[level] || modules.notifications.info;
|
||||
fn.call(modules.notifications, message);
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (!bytes) return '0 Bytes';
|
||||
const units = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
const value = bytes / Math.pow(1024, i);
|
||||
return `${value.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
window.ExcelAnalyzer = {
|
||||
appState,
|
||||
modules,
|
||||
config,
|
||||
init
|
||||
};
|
||||
|
|
@ -0,0 +1,654 @@
|
|||
/**
|
||||
* Analytics Module of Web Application
|
||||
*
|
||||
* Handles data analysis, statistical calculations, and insights generation.
|
||||
*/
|
||||
|
||||
class Analytics {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.analysisCache = new Map();
|
||||
this.maxCacheSize = 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze data structure
|
||||
*/
|
||||
analyzeDataStructure(data) {
|
||||
if (!data || data.length === 0) {
|
||||
return {
|
||||
columns: [],
|
||||
rowCount: 0,
|
||||
columnCount: 0,
|
||||
dataTypes: {},
|
||||
missingValues: {},
|
||||
uniqueValues: {},
|
||||
statistics: {}
|
||||
};
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const rowCount = data.length;
|
||||
const columnCount = headers.length;
|
||||
|
||||
// Analyze each column
|
||||
const columns = headers.map(header => {
|
||||
const values = data.map(row => row[header]);
|
||||
const dataType = this.detectDataType(values);
|
||||
const missingCount = this.countMissingValues(values);
|
||||
const uniqueCount = this.countUniqueValues(values);
|
||||
|
||||
return {
|
||||
name: header,
|
||||
type: dataType,
|
||||
missingCount: missingCount,
|
||||
uniqueValues: uniqueCount,
|
||||
sampleValues: values.slice(0, 5)
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate statistics for numeric columns
|
||||
const statistics = this.calculateStatistics(data, headers);
|
||||
|
||||
return {
|
||||
columns: columns,
|
||||
rowCount: rowCount,
|
||||
columnCount: columnCount,
|
||||
dataTypes: this.getColumnDataTypes(columns),
|
||||
missingValues: this.getMissingValuesSummary(columns),
|
||||
uniqueValues: this.getUniqueValuesSummary(columns),
|
||||
statistics: statistics
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect data type for a column
|
||||
*/
|
||||
detectDataType(values) {
|
||||
if (values.length === 0) return 'empty';
|
||||
|
||||
// Remove null/undefined values for analysis
|
||||
const cleanValues = values.filter(v => v !== null && v !== undefined && v !== '');
|
||||
|
||||
if (cleanValues.length === 0) return 'empty';
|
||||
|
||||
// Check for date patterns
|
||||
const datePatterns = [
|
||||
/^\d{4}-\d{2}-\d{2}$/,
|
||||
/^\d{2}\/\d{2}\/\d{4}$/,
|
||||
/^\d{2}-\d{2}-\d{4}$/,
|
||||
/^\d{4}\/\d{2}\/\d{2}$/
|
||||
];
|
||||
|
||||
let dateCount = 0;
|
||||
let numericCount = 0;
|
||||
let booleanCount = 0;
|
||||
|
||||
for (const value of cleanValues) {
|
||||
const strValue = String(value).trim();
|
||||
|
||||
// Check for date patterns
|
||||
if (datePatterns.some(pattern => pattern.test(strValue))) {
|
||||
if (!isNaN(Date.parse(strValue))) {
|
||||
dateCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for numeric values
|
||||
if (!isNaN(parseFloat(strValue)) && isFinite(strValue)) {
|
||||
numericCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for boolean values
|
||||
if (['true', 'false', 'yes', 'no', '1', '0'].includes(strValue.toLowerCase())) {
|
||||
booleanCount++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine type based on majority
|
||||
const total = cleanValues.length;
|
||||
const dateRatio = dateCount / total;
|
||||
const numericRatio = numericCount / total;
|
||||
const booleanRatio = booleanCount / total;
|
||||
|
||||
if (dateRatio > 0.8) return 'date';
|
||||
if (numericRatio > 0.8) return 'numeric';
|
||||
if (booleanRatio > 0.8) return 'boolean';
|
||||
|
||||
return 'categorical';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count missing values
|
||||
*/
|
||||
countMissingValues(values) {
|
||||
return values.filter(v => v === null || v === undefined || v === '').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count unique values
|
||||
*/
|
||||
countUniqueValues(values) {
|
||||
const unique = new Set(values.map(v => String(v)));
|
||||
return unique.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate statistics for numeric columns
|
||||
*/
|
||||
calculateStatistics(data, headers) {
|
||||
const stats = {};
|
||||
|
||||
headers.forEach(header => {
|
||||
const values = data.map(row => parseFloat(row[header])).filter(v => !isNaN(v));
|
||||
|
||||
if (values.length > 0) {
|
||||
stats[header] = {
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values),
|
||||
mean: this.calculateMean(values),
|
||||
median: this.calculateMedian(values),
|
||||
stdDev: this.calculateStandardDeviation(values),
|
||||
variance: this.calculateVariance(values),
|
||||
sum: this.calculateSum(values),
|
||||
count: values.length,
|
||||
quartiles: this.calculateQuartiles(values)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate mean
|
||||
*/
|
||||
calculateMean(values) {
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate median
|
||||
*/
|
||||
calculateMedian(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
|
||||
if (sorted.length % 2 === 0) {
|
||||
return (sorted[mid - 1] + sorted[mid]) / 2;
|
||||
} else {
|
||||
return sorted[mid];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate standard deviation
|
||||
*/
|
||||
calculateStandardDeviation(values) {
|
||||
const mean = this.calculateMean(values);
|
||||
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate variance
|
||||
*/
|
||||
calculateVariance(values) {
|
||||
const mean = this.calculateMean(values);
|
||||
return values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate sum
|
||||
*/
|
||||
calculateSum(values) {
|
||||
return values.reduce((sum, val) => sum + val, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate quartiles
|
||||
*/
|
||||
calculateQuartiles(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const q1 = this.calculatePercentile(sorted, 25);
|
||||
const q2 = this.calculatePercentile(sorted, 50); // median
|
||||
const q3 = this.calculatePercentile(sorted, 75);
|
||||
|
||||
return {
|
||||
q1: q1,
|
||||
q2: q2,
|
||||
q3: q3,
|
||||
iqr: q3 - q1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentile
|
||||
*/
|
||||
calculatePercentile(sortedValues, percentile) {
|
||||
const index = (percentile / 100) * (sortedValues.length - 1);
|
||||
const lower = Math.floor(index);
|
||||
const upper = Math.ceil(index);
|
||||
const weight = index % 1;
|
||||
|
||||
if (upper >= sortedValues.length) return sortedValues[sortedValues.length - 1];
|
||||
|
||||
return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column data types summary
|
||||
*/
|
||||
getColumnDataTypes(columns) {
|
||||
const types = {};
|
||||
columns.forEach(col => {
|
||||
types[col.name] = col.type;
|
||||
});
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missing values summary
|
||||
*/
|
||||
getMissingValuesSummary(columns) {
|
||||
const summary = {};
|
||||
columns.forEach(col => {
|
||||
summary[col.name] = col.missingCount;
|
||||
});
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique values summary
|
||||
*/
|
||||
getUniqueValuesSummary(columns) {
|
||||
const summary = {};
|
||||
columns.forEach(col => {
|
||||
summary[col.name] = col.uniqueValues;
|
||||
});
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect outliers using IQR method
|
||||
*/
|
||||
detectOutliers(data, columnName, method = 'iqr') {
|
||||
const values = data.map(row => parseFloat(row[columnName])).filter(v => !isNaN(v));
|
||||
|
||||
if (values.length === 0) return { outliers: [], bounds: null };
|
||||
|
||||
if (method === 'iqr') {
|
||||
return this.detectOutliersIQR(values);
|
||||
} else if (method === 'zscore') {
|
||||
return this.detectOutliersZScore(values);
|
||||
}
|
||||
|
||||
return { outliers: [], bounds: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect outliers using IQR method
|
||||
*/
|
||||
detectOutliersIQR(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const q1 = this.calculatePercentile(sorted, 25);
|
||||
const q3 = this.calculatePercentile(sorted, 75);
|
||||
const iqr = q3 - q1;
|
||||
|
||||
const lowerBound = q1 - 1.5 * iqr;
|
||||
const upperBound = q3 + 1.5 * iqr;
|
||||
|
||||
const outliers = values.filter(v => v < lowerBound || v > upperBound);
|
||||
|
||||
return {
|
||||
outliers: outliers,
|
||||
bounds: { lower: lowerBound, upper: upperBound },
|
||||
count: outliers.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect outliers using Z-score method
|
||||
*/
|
||||
detectOutliersZScore(values, threshold = 3) {
|
||||
const mean = this.calculateMean(values);
|
||||
const stdDev = this.calculateStandardDeviation(values);
|
||||
|
||||
const outliers = values.filter(v => Math.abs((v - mean) / stdDev) > threshold);
|
||||
|
||||
return {
|
||||
outliers: outliers,
|
||||
bounds: { mean: mean, stdDev: stdDev, threshold: threshold },
|
||||
count: outliers.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate correlation between two columns
|
||||
*/
|
||||
calculateCorrelation(data, column1, column2) {
|
||||
const x = data.map(row => parseFloat(row[column1])).filter(v => !isNaN(v));
|
||||
const y = data.map(row => parseFloat(row[column2])).filter(v => !isNaN(v));
|
||||
|
||||
if (x.length !== y.length || x.length === 0) {
|
||||
return { correlation: 0, strength: 'none', direction: 'none' };
|
||||
}
|
||||
|
||||
const n = x.length;
|
||||
const sumX = x.reduce((a, b) => a + b, 0);
|
||||
const sumY = y.reduce((a, b) => a + b, 0);
|
||||
const sumXY = x.reduce((sum, val, i) => sum + val * y[i], 0);
|
||||
const sumX2 = x.reduce((sum, val) => sum + val * val, 0);
|
||||
const sumY2 = y.reduce((sum, val) => sum + val * val, 0);
|
||||
|
||||
const numerator = n * sumXY - sumX * sumY;
|
||||
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
|
||||
|
||||
if (denominator === 0) {
|
||||
return { correlation: 0, strength: 'none', direction: 'none' };
|
||||
}
|
||||
|
||||
const correlation = numerator / denominator;
|
||||
|
||||
return {
|
||||
correlation: correlation,
|
||||
strength: this.getCorrelationStrength(Math.abs(correlation)),
|
||||
direction: correlation > 0 ? 'positive' : 'negative'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get correlation strength description
|
||||
*/
|
||||
getCorrelationStrength(value) {
|
||||
if (value < 0.3) return 'weak';
|
||||
if (value < 0.7) return 'moderate';
|
||||
return 'strong';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate insights
|
||||
*/
|
||||
generateInsights(dataStructure) {
|
||||
const insights = [];
|
||||
|
||||
// Data quality insights
|
||||
const totalMissing = Object.values(dataStructure.missingValues).reduce((sum, val) => sum + val, 0);
|
||||
const totalCells = dataStructure.rowCount * dataStructure.columnCount;
|
||||
const dataQuality = ((totalCells - totalMissing) / totalCells) * 100;
|
||||
|
||||
if (dataQuality < 80) {
|
||||
insights.push({
|
||||
type: 'data_quality',
|
||||
severity: 'warning',
|
||||
message: `Data quality is ${dataQuality.toFixed(1)}%. Consider cleaning missing values.`,
|
||||
details: { quality: dataQuality, missing: totalMissing, total: totalCells }
|
||||
});
|
||||
}
|
||||
|
||||
// Column type insights
|
||||
const numericColumns = dataStructure.columns.filter(col => col.type === 'numeric');
|
||||
const categoricalColumns = dataStructure.columns.filter(col => col.type === 'categorical');
|
||||
|
||||
if (numericColumns.length > 1) {
|
||||
insights.push({
|
||||
type: 'analysis_opportunity',
|
||||
severity: 'info',
|
||||
message: `Found ${numericColumns.length} numeric columns. Consider correlation analysis.`,
|
||||
details: { columns: numericColumns.map(c => c.name) }
|
||||
});
|
||||
}
|
||||
|
||||
if (categoricalColumns.length > 0) {
|
||||
insights.push({
|
||||
type: 'visualization_opportunity',
|
||||
severity: 'info',
|
||||
message: `Found ${categoricalColumns.length} categorical columns. Good for bar charts and pie charts.`,
|
||||
details: { columns: categoricalColumns.map(c => c.name) }
|
||||
});
|
||||
}
|
||||
|
||||
// Outlier insights
|
||||
numericColumns.forEach(col => {
|
||||
const stats = dataStructure.statistics[col.name];
|
||||
if (stats) {
|
||||
const outliers = this.detectOutliers(dataStructure.rawData, col.name);
|
||||
if (outliers.count > 0) {
|
||||
insights.push({
|
||||
type: 'outliers',
|
||||
severity: 'warning',
|
||||
message: `Column "${col.name}" has ${outliers.count} outliers.`,
|
||||
details: { column: col.name, count: outliers.count, bounds: outliers.bounds }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return insights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform trend analysis
|
||||
*/
|
||||
analyzeTrends(data, timeColumn, valueColumn) {
|
||||
const timeValues = data.map(row => row[timeColumn]);
|
||||
const numericValues = data.map(row => parseFloat(row[valueColumn])).filter(v => !isNaN(v));
|
||||
|
||||
if (timeValues.length === 0 || numericValues.length === 0) {
|
||||
return { trend: 'none', slope: 0, rSquared: 0 };
|
||||
}
|
||||
|
||||
// Simple linear regression
|
||||
const n = numericValues.length;
|
||||
const sumX = timeValues.reduce((sum, val, i) => sum + i, 0);
|
||||
const sumY = numericValues.reduce((sum, val) => sum + val, 0);
|
||||
const sumXY = timeValues.reduce((sum, val, i) => sum + i * numericValues[i], 0);
|
||||
const sumX2 = timeValues.reduce((sum, val, i) => sum + i * i, 0);
|
||||
|
||||
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
||||
const intercept = (sumY - slope * sumX) / n;
|
||||
|
||||
// Calculate R-squared
|
||||
const yMean = sumY / n;
|
||||
const ssRes = numericValues.reduce((sum, val, i) => {
|
||||
const predicted = slope * i + intercept;
|
||||
return sum + Math.pow(val - predicted, 2);
|
||||
}, 0);
|
||||
const ssTot = numericValues.reduce((sum, val) => sum + Math.pow(val - yMean, 2), 0);
|
||||
const rSquared = 1 - (ssRes / ssTot);
|
||||
|
||||
let trend = 'none';
|
||||
if (Math.abs(slope) > 0.1) {
|
||||
trend = slope > 0 ? 'increasing' : 'decreasing';
|
||||
}
|
||||
|
||||
return {
|
||||
trend: trend,
|
||||
slope: slope,
|
||||
rSquared: rSquared,
|
||||
intercept: intercept,
|
||||
equation: `y = ${slope.toFixed(4)}x + ${intercept.toFixed(4)}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache analysis results
|
||||
*/
|
||||
cacheAnalysis(key, result) {
|
||||
if (this.analysisCache.size >= this.maxCacheSize) {
|
||||
// Remove oldest entry
|
||||
const oldestKey = this.analysisCache.keys().next().value;
|
||||
this.analysisCache.delete(oldestKey);
|
||||
}
|
||||
|
||||
this.analysisCache.set(key, {
|
||||
result: result,
|
||||
timestamp: Date.now(),
|
||||
size: JSON.stringify(result).length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached analysis
|
||||
*/
|
||||
getCachedAnalysis(key) {
|
||||
const cached = this.analysisCache.get(key);
|
||||
if (cached) {
|
||||
// Check if cache is still valid (10 minutes)
|
||||
if (Date.now() - cached.timestamp < 600000) {
|
||||
return cached.result;
|
||||
} else {
|
||||
this.analysisCache.delete(key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
*/
|
||||
clearCache() {
|
||||
this.analysisCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getCacheStats() {
|
||||
return {
|
||||
size: this.analysisCache.size,
|
||||
maxSize: this.maxCacheSize,
|
||||
entries: Array.from(this.analysisCache.keys()),
|
||||
totalSize: Array.from(this.analysisCache.values()).reduce((sum, entry) => sum + entry.size, 0)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform comprehensive analysis
|
||||
*/
|
||||
async performAnalysis(data, options = {}) {
|
||||
const cacheKey = this.generateCacheKey(data, options);
|
||||
const cached = this.getCachedAnalysis(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Perform analysis
|
||||
const structure = this.analyzeDataStructure(data);
|
||||
const insights = this.generateInsights(structure);
|
||||
|
||||
const result = {
|
||||
structure: structure,
|
||||
insights: insights,
|
||||
analysisTime: Date.now() - startTime,
|
||||
timestamp: Date.now(),
|
||||
options: options
|
||||
};
|
||||
|
||||
// Cache result
|
||||
this.cacheAnalysis(cacheKey, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key
|
||||
*/
|
||||
generateCacheKey(data, options) {
|
||||
const dataHash = this.hashData(data);
|
||||
const optionsHash = JSON.stringify(options);
|
||||
return `${dataHash}-${optionsHash}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple data hash function
|
||||
*/
|
||||
hashData(data) {
|
||||
const str = JSON.stringify(data.slice(0, 100)); // Hash only first 100 rows
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export analysis report
|
||||
*/
|
||||
exportAnalysisReport(analysisResult) {
|
||||
const report = {
|
||||
metadata: {
|
||||
title: 'Data Analysis Report',
|
||||
generatedAt: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
},
|
||||
summary: {
|
||||
rowCount: analysisResult.structure.rowCount,
|
||||
columnCount: analysisResult.structure.columnCount,
|
||||
analysisTime: analysisResult.analysisTime
|
||||
},
|
||||
dataStructure: analysisResult.structure,
|
||||
insights: analysisResult.insights,
|
||||
recommendations: this.generateRecommendations(analysisResult)
|
||||
};
|
||||
|
||||
return JSON.stringify(report, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate recommendations based on analysis
|
||||
*/
|
||||
generateRecommendations(analysisResult) {
|
||||
const recommendations = [];
|
||||
|
||||
// Data quality recommendations
|
||||
const dataQuality = analysisResult.insights.find(i => i.type === 'data_quality');
|
||||
if (dataQuality && dataQuality.severity === 'warning') {
|
||||
recommendations.push({
|
||||
category: 'Data Quality',
|
||||
priority: 'high',
|
||||
action: 'Clean missing values',
|
||||
description: 'Consider filling or removing missing data to improve analysis quality.'
|
||||
});
|
||||
}
|
||||
|
||||
// Visualization recommendations
|
||||
const vizOpportunities = analysisResult.insights.filter(i => i.type === 'visualization_opportunity');
|
||||
if (vizOpportunities.length > 0) {
|
||||
recommendations.push({
|
||||
category: 'Visualization',
|
||||
priority: 'medium',
|
||||
action: 'Create charts',
|
||||
description: 'Generate visualizations for categorical data to identify patterns.'
|
||||
});
|
||||
}
|
||||
|
||||
// Analysis recommendations
|
||||
const analysisOpportunities = analysisResult.insights.filter(i => i.type === 'analysis_opportunity');
|
||||
if (analysisOpportunities.length > 0) {
|
||||
recommendations.push({
|
||||
category: 'Analysis',
|
||||
priority: 'medium',
|
||||
action: 'Perform correlation analysis',
|
||||
description: 'Analyze relationships between numeric variables.'
|
||||
});
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Analytics;
|
||||
} else {
|
||||
window.Analytics = Analytics;
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Dashboard Module of Web Application
|
||||
*
|
||||
* Renders lightweight KPI and chart widgets into the dashboard section.
|
||||
*/
|
||||
|
||||
class DashboardManager {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.containerId = options.containerId || 'dashboardGrid';
|
||||
this.container = document.getElementById(this.containerId);
|
||||
this.layout = 'layout1';
|
||||
}
|
||||
|
||||
setLayout(layout) {
|
||||
this.layout = layout || 'layout1';
|
||||
if (this.container) {
|
||||
this.container.dataset.layout = this.layout;
|
||||
}
|
||||
this.emitUpdate();
|
||||
}
|
||||
|
||||
render({ data = [], structure = null, chartDataUrl = null } = {}) {
|
||||
if (!this.container) return;
|
||||
|
||||
const stats = this.calculateStats(data, structure);
|
||||
const chartCard = chartDataUrl
|
||||
? `
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><strong>Latest Chart Snapshot</strong></div>
|
||||
<div class="card-body text-center">
|
||||
<img src="${chartDataUrl}" alt="Chart snapshot" style="max-width:100%;height:auto;border:1px solid var(--border-color);border-radius:6px;" />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><strong>Latest Chart Snapshot</strong></div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center text-muted">
|
||||
No chart rendered yet.
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const columns = (structure?.columns || [])
|
||||
.slice(0, 8)
|
||||
.map((c) => `<li class="list-group-item d-flex justify-content-between"><span>${c.name}</span><small class="text-muted">${c.type}</small></li>`)
|
||||
.join('');
|
||||
|
||||
this.container.innerHTML = `
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><strong>Dataset Summary</strong></div>
|
||||
<div class="card-body">
|
||||
<p class="mb-1"><strong>Rows:</strong> ${stats.rows}</p>
|
||||
<p class="mb-1"><strong>Columns:</strong> ${stats.columns}</p>
|
||||
<p class="mb-1"><strong>Missing:</strong> ${stats.missing}</p>
|
||||
<p class="mb-0"><strong>Quality:</strong> ${stats.quality}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><strong>Columns</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<ul class="list-group list-group-flush">
|
||||
${columns || '<li class="list-group-item text-muted">No column info available.</li>'}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
${chartCard}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.emitUpdate();
|
||||
}
|
||||
|
||||
calculateStats(data, structure) {
|
||||
const rows = Array.isArray(data) ? data.length : 0;
|
||||
const columns = structure?.columnCount || (rows ? Object.keys(data[0]).length : 0);
|
||||
|
||||
let missing = 0;
|
||||
if (rows && columns) {
|
||||
const headers = Object.keys(data[0]);
|
||||
data.forEach((row) => {
|
||||
headers.forEach((h) => {
|
||||
const v = row[h];
|
||||
if (v === null || v === undefined || v === '') missing += 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const totalCells = rows * columns;
|
||||
const quality = totalCells ? Math.round(((totalCells - missing) / totalCells) * 100) : 100;
|
||||
|
||||
return { rows, columns, missing, quality };
|
||||
}
|
||||
|
||||
exportState() {
|
||||
return {
|
||||
layout: this.layout,
|
||||
html: this.container ? this.container.innerHTML : ''
|
||||
};
|
||||
}
|
||||
|
||||
emitUpdate() {
|
||||
if (typeof this.options.onDashboardUpdated === 'function') {
|
||||
this.options.onDashboardUpdated(this.exportState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = DashboardManager;
|
||||
} else {
|
||||
window.DashboardManager = DashboardManager;
|
||||
}
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
/**
|
||||
* Data Processing Module of Web Application
|
||||
*
|
||||
* Handles data analysis, preprocessing, missing value detection,
|
||||
* outlier identification, and data cleaning.
|
||||
*/
|
||||
|
||||
class DataProcessor {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze data structure and characteristics
|
||||
*/
|
||||
analyzeDataStructure(data) {
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error('No data to analyze');
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const rowCount = data.length;
|
||||
|
||||
const columns = headers.map(header => {
|
||||
const values = data.map(row => row[header]);
|
||||
return this.analyzeColumn(header, values);
|
||||
});
|
||||
|
||||
const structure = {
|
||||
rowCount,
|
||||
columnCount: headers.length,
|
||||
columns,
|
||||
totalCells: rowCount * headers.length,
|
||||
missingCells: this.countMissingValues(data),
|
||||
dataTypes: this.detectDataTypes(data)
|
||||
};
|
||||
|
||||
if (this.options.onDataAnalyzed) {
|
||||
this.options.onDataAnalyzed(structure);
|
||||
}
|
||||
|
||||
return structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze individual column characteristics
|
||||
*/
|
||||
analyzeColumn(name, values) {
|
||||
const nonNullValues = values.filter(v => v !== null && v !== undefined && v !== '');
|
||||
const nullCount = values.length - nonNullValues.length;
|
||||
const uniqueValues = new Set(nonNullValues.map(v => v.toString())).size;
|
||||
|
||||
const dataType = this.detectColumnType(nonNullValues);
|
||||
const statistics = this.calculateStatistics(nonNullValues, dataType);
|
||||
|
||||
return {
|
||||
name,
|
||||
type: dataType,
|
||||
uniqueValues,
|
||||
nullCount,
|
||||
nullPercentage: Math.round((nullCount / values.length) * 100),
|
||||
statistics
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect column data type
|
||||
*/
|
||||
detectColumnType(values) {
|
||||
if (values.length === 0) return 'unknown';
|
||||
|
||||
// Check for date/time
|
||||
const datePattern = /^\d{4}-\d{2}-\d{2}/;
|
||||
const timePattern = /^\d{2}:\d{2}:\d{2}/;
|
||||
|
||||
for (let value of values) {
|
||||
if (typeof value === 'string') {
|
||||
if (datePattern.test(value) || timePattern.test(value)) {
|
||||
return 'datetime';
|
||||
}
|
||||
if (!isNaN(Date.parse(value))) {
|
||||
return 'datetime';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for numeric
|
||||
const numericValues = values.filter(v => !isNaN(v) && v !== '');
|
||||
if (numericValues.length / values.length > 0.8) {
|
||||
return 'numeric';
|
||||
}
|
||||
|
||||
// Check for boolean
|
||||
const booleanValues = values.filter(v =>
|
||||
v === true || v === false ||
|
||||
v === 'true' || v === 'false' ||
|
||||
v === 'TRUE' || v === 'FALSE' ||
|
||||
v === 0 || v === 1
|
||||
);
|
||||
if (booleanValues.length / values.length > 0.9) {
|
||||
return 'boolean';
|
||||
}
|
||||
|
||||
// Default to text
|
||||
return 'text';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate column statistics
|
||||
*/
|
||||
calculateStatistics(values, dataType) {
|
||||
if (dataType !== 'numeric' || values.length === 0) {
|
||||
return {
|
||||
min: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
median: null,
|
||||
stdDev: null,
|
||||
outliers: []
|
||||
};
|
||||
}
|
||||
|
||||
const numericValues = values.map(v => parseFloat(v)).filter(v => !isNaN(v));
|
||||
|
||||
if (numericValues.length === 0) {
|
||||
return { min: null, max: null, mean: null, median: null, stdDev: null, outliers: [] };
|
||||
}
|
||||
|
||||
numericValues.sort((a, b) => a - b);
|
||||
|
||||
const min = Math.min(...numericValues);
|
||||
const max = Math.max(...numericValues);
|
||||
const mean = this.calculateMean(numericValues);
|
||||
const median = this.calculateMedian(numericValues);
|
||||
const stdDev = this.calculateStdDev(numericValues, mean);
|
||||
const outliers = this.detectOutliers(numericValues, mean, stdDev);
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
mean,
|
||||
median,
|
||||
stdDev,
|
||||
outliers
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate mean
|
||||
*/
|
||||
calculateMean(values) {
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate median
|
||||
*/
|
||||
calculateMedian(values) {
|
||||
const mid = Math.floor(values.length / 2);
|
||||
return values.length % 2 !== 0
|
||||
? values[mid]
|
||||
: (values[mid - 1] + values[mid]) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate standard deviation
|
||||
*/
|
||||
calculateStdDev(values, mean) {
|
||||
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect outliers using IQR method
|
||||
*/
|
||||
detectOutliers(values, mean, stdDev) {
|
||||
// Using Z-score method for outlier detection
|
||||
const threshold = 3;
|
||||
const outliers = [];
|
||||
|
||||
values.forEach((value, index) => {
|
||||
const zScore = Math.abs((value - mean) / stdDev);
|
||||
if (zScore > threshold) {
|
||||
outliers.push({
|
||||
index,
|
||||
value,
|
||||
zScore
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return outliers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count missing values in dataset
|
||||
*/
|
||||
countMissingValues(data) {
|
||||
let count = 0;
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
data.forEach(row => {
|
||||
headers.forEach(header => {
|
||||
if (row[header] === null || row[header] === undefined || row[header] === '') {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect data types for all columns
|
||||
*/
|
||||
detectDataTypes(data) {
|
||||
if (data.length === 0) return {};
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const types = {};
|
||||
|
||||
headers.forEach(header => {
|
||||
const values = data.map(row => row[header]);
|
||||
types[header] = this.detectColumnType(values);
|
||||
});
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean data by handling missing values and outliers
|
||||
*/
|
||||
cleanData(data, rules = {}) {
|
||||
if (!data || data.length === 0) return data;
|
||||
|
||||
const cleanedData = JSON.parse(JSON.stringify(data)); // Deep copy
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
headers.forEach(header => {
|
||||
const columnRules = rules[header] || {};
|
||||
const values = cleanedData.map(row => row[header]);
|
||||
const dataType = this.detectColumnType(values);
|
||||
|
||||
// Handle missing values
|
||||
if (columnRules.handleMissing) {
|
||||
cleanedData.forEach(row => {
|
||||
if (row[header] === null || row[header] === undefined || row[header] === '') {
|
||||
row[header] = this.handleMissingValue(row[header], dataType, columnRules);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle outliers
|
||||
if (columnRules.handleOutliers && dataType === 'numeric') {
|
||||
const numericValues = values
|
||||
.filter(v => !isNaN(v) && v !== '')
|
||||
.map(v => parseFloat(v));
|
||||
|
||||
if (numericValues.length > 0) {
|
||||
const mean = this.calculateMean(numericValues);
|
||||
const stdDev = this.calculateStdDev(numericValues, mean);
|
||||
|
||||
cleanedData.forEach(row => {
|
||||
const value = parseFloat(row[header]);
|
||||
if (!isNaN(value)) {
|
||||
const zScore = Math.abs((value - mean) / stdDev);
|
||||
if (zScore > 3) { // Outlier threshold
|
||||
row[header] = this.handleOutlier(value, mean, stdDev, columnRules);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.options.onDataProcessed) {
|
||||
this.options.onDataProcessed(cleanedData);
|
||||
}
|
||||
|
||||
return cleanedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle missing values based on strategy
|
||||
*/
|
||||
handleMissingValue(value, dataType, rules) {
|
||||
const strategy = rules.missingStrategy || 'remove';
|
||||
|
||||
switch (strategy) {
|
||||
case 'mean':
|
||||
return dataType === 'numeric' ? 0 : '';
|
||||
case 'median':
|
||||
return dataType === 'numeric' ? 0 : '';
|
||||
case 'mode':
|
||||
return '';
|
||||
case 'forward_fill':
|
||||
return value; // Would need previous value
|
||||
case 'backward_fill':
|
||||
return value; // Would need next value
|
||||
case 'remove':
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle outliers based on strategy
|
||||
*/
|
||||
handleOutlier(value, mean, stdDev, rules) {
|
||||
const strategy = rules.outlierStrategy || 'remove';
|
||||
|
||||
switch (strategy) {
|
||||
case 'cap':
|
||||
return Math.sign(value) * 3 * stdDev + mean;
|
||||
case 'mean':
|
||||
return mean;
|
||||
case 'remove':
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data quality report
|
||||
*/
|
||||
getDataQualityReport(data) {
|
||||
if (!data || data.length === 0) {
|
||||
return { quality: 0, issues: ['No data available'] };
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const totalCells = data.length * headers.length;
|
||||
const missingCells = this.countMissingValues(data);
|
||||
const missingPercentage = (missingCells / totalCells) * 100;
|
||||
|
||||
const issues = [];
|
||||
let quality = 100;
|
||||
|
||||
if (missingPercentage > 10) {
|
||||
issues.push(`High missing value rate: ${missingPercentage.toFixed(1)}%`);
|
||||
quality -= 20;
|
||||
} else if (missingPercentage > 5) {
|
||||
issues.push(`Moderate missing value rate: ${missingPercentage.toFixed(1)}%`);
|
||||
quality -= 10;
|
||||
}
|
||||
|
||||
// Check for data type inconsistencies
|
||||
headers.forEach(header => {
|
||||
const values = data.map(row => row[header]);
|
||||
const dataType = this.detectColumnType(values);
|
||||
|
||||
if (dataType === 'unknown') {
|
||||
issues.push(`Column "${header}" has inconsistent data types`);
|
||||
quality -= 5;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
quality: Math.max(0, quality),
|
||||
missingPercentage,
|
||||
issues,
|
||||
totalRecords: data.length,
|
||||
totalFields: headers.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = DataProcessor;
|
||||
} else {
|
||||
window.DataProcessor = DataProcessor;
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
/**
|
||||
* Export Module of Web Application
|
||||
*
|
||||
* Handles export to PDF, Excel, CSV, JSON and PNG chart image.
|
||||
*/
|
||||
|
||||
class Exporter {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
async export(format, options = {}) {
|
||||
const normalizedFormat = String(format || '').toLowerCase();
|
||||
const payload = this.preparePayload(options);
|
||||
|
||||
this.emitProgress(15, 'Preparing export data...');
|
||||
|
||||
let result;
|
||||
switch (normalizedFormat) {
|
||||
case 'pdf':
|
||||
result = await this.exportPDF(payload, options);
|
||||
break;
|
||||
case 'excel':
|
||||
case 'xlsx':
|
||||
result = await this.exportExcel(payload, options);
|
||||
break;
|
||||
case 'csv':
|
||||
result = await this.exportCSV(payload, options);
|
||||
break;
|
||||
case 'json':
|
||||
result = await this.exportJSON(payload, options);
|
||||
break;
|
||||
case 'png':
|
||||
result = await this.exportPNG(payload, options);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported export format: ${format}`);
|
||||
}
|
||||
|
||||
this.emitProgress(90, 'Downloading file...');
|
||||
this.downloadBlob(result.blob, result.filename);
|
||||
this.emitProgress(100, 'Export complete.');
|
||||
|
||||
if (typeof this.options.onExportComplete === 'function') {
|
||||
this.options.onExportComplete({
|
||||
format: normalizedFormat,
|
||||
filename: result.filename
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
preparePayload(options = {}) {
|
||||
return {
|
||||
rawData: Array.isArray(options.rawData) ? options.rawData : [],
|
||||
processedData: Array.isArray(options.processedData) ? options.processedData : [],
|
||||
dataStructure: options.dataStructure || null,
|
||||
title: options.title || 'Excel Analyzer Report',
|
||||
chart: options.currentChart || null,
|
||||
exportedAt: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
getFileBase(options = {}) {
|
||||
const rawBase = options.filenameBase || 'excel-analyzer-export';
|
||||
return rawBase.replace(/[<>:"/\\|?*]+/g, '-');
|
||||
}
|
||||
|
||||
getTimestamp() {
|
||||
return new Date().toISOString().replace(/[:.]/g, '-');
|
||||
}
|
||||
|
||||
async exportPDF(payload, options = {}) {
|
||||
if (!window.jspdf || !window.jspdf.jsPDF) {
|
||||
throw new Error('jsPDF is not available in this page.');
|
||||
}
|
||||
|
||||
const doc = new window.jspdf.jsPDF();
|
||||
const lineHeight = 7;
|
||||
let y = 15;
|
||||
|
||||
doc.setFontSize(16);
|
||||
doc.text(payload.title, 14, y);
|
||||
y += 10;
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.text(`Exported: ${payload.exportedAt.toLocaleString()}`, 14, y);
|
||||
y += lineHeight;
|
||||
doc.text(`Rows: ${payload.processedData.length}`, 14, y);
|
||||
y += lineHeight;
|
||||
doc.text(`Columns: ${payload.processedData.length ? Object.keys(payload.processedData[0]).length : 0}`, 14, y);
|
||||
y += lineHeight * 2;
|
||||
|
||||
const previewRows = payload.processedData.slice(0, 20);
|
||||
if (previewRows.length > 0) {
|
||||
const headers = Object.keys(previewRows[0]);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.text(headers.join(' | ').slice(0, 180), 14, y);
|
||||
y += lineHeight;
|
||||
doc.setFont(undefined, 'normal');
|
||||
|
||||
previewRows.forEach((row) => {
|
||||
if (y > 280) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
const line = headers.map((h) => (row[h] ?? '')).join(' | ');
|
||||
doc.text(String(line).slice(0, 180), 14, y);
|
||||
y += lineHeight;
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.chart && typeof payload.chart.toBase64Image === 'function') {
|
||||
try {
|
||||
const img = payload.chart.toBase64Image('image/png', 1);
|
||||
doc.addPage();
|
||||
doc.setFontSize(12);
|
||||
doc.text('Chart Snapshot', 14, 15);
|
||||
doc.addImage(img, 'PNG', 14, 24, 180, 100);
|
||||
} catch (error) {
|
||||
console.warn('Could not embed chart image in PDF.', error);
|
||||
}
|
||||
}
|
||||
|
||||
const blob = doc.output('blob');
|
||||
const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.pdf`;
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
async exportExcel(payload, options = {}) {
|
||||
if (!window.XLSX) {
|
||||
throw new Error('SheetJS (XLSX) is not available in this page.');
|
||||
}
|
||||
|
||||
const workbook = window.XLSX.utils.book_new();
|
||||
const data = payload.processedData.length ? payload.processedData : payload.rawData;
|
||||
|
||||
const dataSheet = window.XLSX.utils.json_to_sheet(data);
|
||||
window.XLSX.utils.book_append_sheet(workbook, dataSheet, 'Processed Data');
|
||||
|
||||
const summaryRows = [
|
||||
{ Metric: 'Title', Value: payload.title },
|
||||
{ Metric: 'Exported At', Value: payload.exportedAt.toLocaleString() },
|
||||
{ Metric: 'Total Rows', Value: data.length },
|
||||
{ Metric: 'Total Columns', Value: data.length ? Object.keys(data[0]).length : 0 }
|
||||
];
|
||||
const summarySheet = window.XLSX.utils.json_to_sheet(summaryRows);
|
||||
window.XLSX.utils.book_append_sheet(workbook, summarySheet, 'Summary');
|
||||
|
||||
const arrayBuffer = window.XLSX.write(workbook, {
|
||||
bookType: 'xlsx',
|
||||
type: 'array'
|
||||
});
|
||||
|
||||
const blob = new Blob([arrayBuffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.xlsx`;
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
async exportCSV(payload, options = {}) {
|
||||
const data = payload.processedData.length ? payload.processedData : payload.rawData;
|
||||
const csv = this.toCSV(data);
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
||||
const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.csv`;
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
async exportJSON(payload, options = {}) {
|
||||
const content = JSON.stringify(
|
||||
{
|
||||
title: payload.title,
|
||||
exportedAt: payload.exportedAt.toISOString(),
|
||||
dataStructure: payload.dataStructure,
|
||||
data: payload.processedData.length ? payload.processedData : payload.rawData
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
const blob = new Blob([content], { type: 'application/json;charset=utf-8' });
|
||||
const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.json`;
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
async exportPNG(payload, options = {}) {
|
||||
if (!payload.chart || typeof payload.chart.toBase64Image !== 'function') {
|
||||
throw new Error('No chart is available yet for PNG export. Render a chart first.');
|
||||
}
|
||||
|
||||
const dataUrl = payload.chart.toBase64Image('image/png', 1);
|
||||
const blob = await (await fetch(dataUrl)).blob();
|
||||
const filename = `${this.getFileBase(options)}-${this.getTimestamp()}.png`;
|
||||
return { blob, filename };
|
||||
}
|
||||
|
||||
toCSV(rows) {
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const headers = Object.keys(rows[0]);
|
||||
const csvRows = [headers.join(',')];
|
||||
|
||||
rows.forEach((row) => {
|
||||
const line = headers
|
||||
.map((header) => {
|
||||
const value = row[header] ?? '';
|
||||
const text = String(value);
|
||||
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
||||
})
|
||||
.join(',');
|
||||
csvRows.push(line);
|
||||
});
|
||||
|
||||
return csvRows.join('\n');
|
||||
}
|
||||
|
||||
downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
emitProgress(percentage, message) {
|
||||
if (typeof this.options.onExportProgress === 'function') {
|
||||
this.options.onExportProgress({ percentage, message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Exporter;
|
||||
} else {
|
||||
window.Exporter = Exporter;
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
/**
|
||||
* File Upload Module of Web Application
|
||||
*
|
||||
* Handles file input, drag-and-drop functionality, file validation,
|
||||
* and Excel file parsing using SheetJS.
|
||||
*/
|
||||
|
||||
class FileUploadManager {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.dropZone = null;
|
||||
this.fileInput = null;
|
||||
this.selectFileBtn = null;
|
||||
this.isProcessing = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.dropZone = document.getElementById('dropZone');
|
||||
this.fileInput = document.getElementById('fileInput');
|
||||
this.selectFileBtn = document.getElementById('selectFileBtn');
|
||||
|
||||
if (!this.dropZone || !this.fileInput) {
|
||||
throw new Error('File upload elements not found in DOM');
|
||||
}
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const openFileDialog = () => {
|
||||
if (this.isProcessing) return;
|
||||
// Reset so selecting the same file again still triggers `change`.
|
||||
this.fileInput.value = '';
|
||||
this.fileInput.click();
|
||||
};
|
||||
|
||||
// File input change event
|
||||
this.fileInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
this.handleFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Drag and drop events
|
||||
this.dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
this.dropZone.classList.add('drag-over');
|
||||
});
|
||||
|
||||
this.dropZone.addEventListener('dragleave', () => {
|
||||
this.dropZone.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
this.dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
this.dropZone.classList.remove('drag-over');
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
this.handleFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Dedicated select button (prevents duplicate bubbling click behavior)
|
||||
if (this.selectFileBtn) {
|
||||
this.selectFileBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openFileDialog();
|
||||
});
|
||||
}
|
||||
|
||||
// Click anywhere on drop zone to upload (except button which handles itself)
|
||||
this.dropZone.addEventListener('click', (e) => {
|
||||
if (e.target && e.target.closest && e.target.closest('#selectFileBtn')) {
|
||||
return;
|
||||
}
|
||||
openFileDialog();
|
||||
});
|
||||
}
|
||||
|
||||
async handleFile(file) {
|
||||
if (this.isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
|
||||
try {
|
||||
// Validate file
|
||||
const validation = this.validateFile(file);
|
||||
if (!validation.isValid) {
|
||||
this.showError(validation.error);
|
||||
this.isProcessing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify file selected
|
||||
if (this.options.onFileSelected) {
|
||||
this.options.onFileSelected(file);
|
||||
}
|
||||
|
||||
// Show progress
|
||||
this.showProgress(0);
|
||||
|
||||
// Parse file
|
||||
const data = await this.parseExcelFile(file);
|
||||
|
||||
// Notify file parsed
|
||||
if (this.options.onFileParsed) {
|
||||
this.options.onFileParsed(data);
|
||||
}
|
||||
|
||||
this.showProgress(100);
|
||||
this.hideProgress();
|
||||
|
||||
} catch (error) {
|
||||
this.showError(error.message);
|
||||
if (this.options.onError) {
|
||||
this.options.onError(error);
|
||||
}
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
this.fileInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
validateFile(file) {
|
||||
// Check file type (MIME + extension fallback)
|
||||
const allowedTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel'
|
||||
];
|
||||
const lowerName = (file.name || '').toLowerCase();
|
||||
const hasValidExtension = lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls');
|
||||
const hasValidMime = allowedTypes.includes(file.type);
|
||||
|
||||
if (!hasValidMime && !hasValidExtension) {
|
||||
return { isValid: false, error: 'Invalid file type. Please upload an Excel file (.xlsx or .xls)' };
|
||||
}
|
||||
|
||||
// Check file size (max 50MB)
|
||||
const maxSize = 50 * 1024 * 1024; // 50MB
|
||||
if (file.size > maxSize) {
|
||||
return { isValid: false, error: 'File too large. Maximum file size is 50MB' };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
async parseExcelFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = new Uint8Array(e.target.result);
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
|
||||
// Get first worksheet
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
// Convert to JSON
|
||||
const jsonData = XLSX.utils.sheet_to_json(worksheet, {
|
||||
header: 1,
|
||||
defval: null
|
||||
});
|
||||
|
||||
// Convert to array of objects
|
||||
const result = this.convertToObjects(jsonData);
|
||||
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(new Error('Failed to parse Excel file: ' + error.message));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
reject(new Error('Failed to read file'));
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
convertToObjects(jsonData) {
|
||||
if (jsonData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// First row contains headers
|
||||
const headers = jsonData[0];
|
||||
const rows = jsonData.slice(1);
|
||||
|
||||
// Clean headers (remove empty headers and create unique names)
|
||||
const cleanHeaders = headers.map((header, index) => {
|
||||
if (!header || header === '') {
|
||||
return `Column_${index + 1}`;
|
||||
}
|
||||
return header.toString().trim();
|
||||
});
|
||||
|
||||
// Create unique column names if duplicates exist
|
||||
const uniqueHeaders = this.makeUniqueHeaders(cleanHeaders);
|
||||
|
||||
// Convert rows to objects
|
||||
return rows.map(row => {
|
||||
const obj = {};
|
||||
uniqueHeaders.forEach((header, index) => {
|
||||
obj[header] = row[index];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
makeUniqueHeaders(headers) {
|
||||
const seen = {};
|
||||
return headers.map(header => {
|
||||
if (seen[header]) {
|
||||
const newHeader = `${header}_${seen[header]}`;
|
||||
seen[header]++;
|
||||
return newHeader;
|
||||
} else {
|
||||
seen[header] = 1;
|
||||
return header;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showProgress(percentage) {
|
||||
const progressEl = document.getElementById('uploadProgress');
|
||||
const progressBarEl = progressEl ? progressEl.querySelector('.progress-bar') : null;
|
||||
|
||||
if (progressEl) {
|
||||
progressEl.style.display = 'block';
|
||||
}
|
||||
|
||||
if (progressBarEl) {
|
||||
progressBarEl.style.width = `${percentage}%`;
|
||||
progressBarEl.textContent = `${percentage}%`;
|
||||
}
|
||||
}
|
||||
|
||||
hideProgress() {
|
||||
const progressEl = document.getElementById('uploadProgress');
|
||||
if (progressEl) {
|
||||
progressEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const statusEl = document.getElementById('uploadStatus');
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = `
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong>Error:</strong> ${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
clearError() {
|
||||
const statusEl = document.getElementById('uploadStatus');
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = FileUploadManager;
|
||||
} else {
|
||||
window.FileUploadManager = FileUploadManager;
|
||||
}
|
||||
|
|
@ -0,0 +1,773 @@
|
|||
/**
|
||||
* Notifications Module
|
||||
*
|
||||
* Handles user notifications, alerts, and feedback messages.
|
||||
*/
|
||||
|
||||
class Notifications {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.notifications = [];
|
||||
this.maxNotifications = 50;
|
||||
this.container = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createContainer();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create notifications container
|
||||
*/
|
||||
createContainer() {
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = 'notifications-container';
|
||||
this.container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-width: 400px;
|
||||
`;
|
||||
|
||||
document.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Auto-dismiss notifications after timeout
|
||||
setInterval(() => {
|
||||
this.notifications.forEach(notification => {
|
||||
if (notification.autoDismiss && Date.now() - notification.timestamp > notification.timeout) {
|
||||
this.removeNotification(notification.id);
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show success notification
|
||||
*/
|
||||
success(message, options = {}) {
|
||||
return this.show({
|
||||
type: 'success',
|
||||
message: message,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error notification
|
||||
*/
|
||||
error(message, options = {}) {
|
||||
return this.show({
|
||||
type: 'error',
|
||||
message: message,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show warning notification
|
||||
*/
|
||||
warning(message, options = {}) {
|
||||
return this.show({
|
||||
type: 'warning',
|
||||
message: message,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show info notification
|
||||
*/
|
||||
info(message, options = {}) {
|
||||
return this.show({
|
||||
type: 'info',
|
||||
message: message,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification
|
||||
*/
|
||||
show(notificationData) {
|
||||
const notification = {
|
||||
id: this.generateId(),
|
||||
type: notificationData.type || 'info',
|
||||
message: notificationData.message || '',
|
||||
title: notificationData.title || this.getNotificationTitle(notificationData.type),
|
||||
timestamp: Date.now(),
|
||||
autoDismiss: notificationData.autoDismiss !== false,
|
||||
timeout: notificationData.timeout || this.getTimeoutForType(notificationData.type),
|
||||
persistent: notificationData.persistent || false,
|
||||
actions: notificationData.actions || [],
|
||||
onDismiss: notificationData.onDismiss || null
|
||||
};
|
||||
|
||||
this.notifications.push(notification);
|
||||
this.renderNotification(notification);
|
||||
|
||||
// Limit notifications
|
||||
if (this.notifications.length > this.maxNotifications) {
|
||||
const oldest = this.notifications.shift();
|
||||
this.removeNotification(oldest.id);
|
||||
}
|
||||
|
||||
return notification.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render notification element
|
||||
*/
|
||||
renderNotification(notification) {
|
||||
const element = document.createElement('div');
|
||||
element.className = `notification notification-${notification.type}`;
|
||||
element.dataset.notificationId = notification.id;
|
||||
element.style.cssText = `
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 15px;
|
||||
min-width: 250px;
|
||||
max-width: 400px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
// Icon
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'notification-icon';
|
||||
icon.innerHTML = this.getNotificationIcon(notification.type);
|
||||
icon.style.cssText = `
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
font-size: 18px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
// Content
|
||||
const content = document.createElement('div');
|
||||
content.className = 'notification-content';
|
||||
content.style.cssText = `
|
||||
margin-left: 35px;
|
||||
min-height: 24px;
|
||||
`;
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'notification-title';
|
||||
title.textContent = notification.title;
|
||||
title.style.cssText = `
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const message = document.createElement('div');
|
||||
message.className = 'notification-message';
|
||||
message.textContent = notification.message;
|
||||
message.style.cssText = `
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
`;
|
||||
|
||||
content.appendChild(title);
|
||||
content.appendChild(message);
|
||||
|
||||
// Actions
|
||||
if (notification.actions.length > 0) {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'notification-actions';
|
||||
actions.style.cssText = `
|
||||
margin-left: 35px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
notification.actions.forEach(action => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'btn btn-sm btn-outline-secondary';
|
||||
button.textContent = action.label;
|
||||
button.style.cssText = `
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (action.onClick) {
|
||||
action.onClick(notification);
|
||||
}
|
||||
this.removeNotification(notification.id);
|
||||
});
|
||||
|
||||
actions.appendChild(button);
|
||||
});
|
||||
|
||||
content.appendChild(actions);
|
||||
}
|
||||
|
||||
// Dismiss button
|
||||
const dismissBtn = document.createElement('button');
|
||||
dismissBtn.className = 'notification-dismiss';
|
||||
dismissBtn.innerHTML = '×';
|
||||
dismissBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
`;
|
||||
|
||||
dismissBtn.addEventListener('mouseenter', () => {
|
||||
dismissBtn.style.backgroundColor = 'var(--bg-secondary)';
|
||||
});
|
||||
|
||||
dismissBtn.addEventListener('mouseleave', () => {
|
||||
dismissBtn.style.backgroundColor = 'transparent';
|
||||
});
|
||||
|
||||
dismissBtn.addEventListener('click', () => {
|
||||
this.removeNotification(notification.id);
|
||||
});
|
||||
|
||||
element.appendChild(icon);
|
||||
element.appendChild(content);
|
||||
element.appendChild(dismissBtn);
|
||||
|
||||
this.container.appendChild(element);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => {
|
||||
element.style.animation = 'none';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove notification
|
||||
*/
|
||||
removeNotification(id) {
|
||||
const index = this.notifications.findIndex(n => n.id === id);
|
||||
if (index === -1) return;
|
||||
|
||||
const notification = this.notifications[index];
|
||||
const element = this.container.querySelector(`[data-notification-id="${id}"]`);
|
||||
|
||||
if (element) {
|
||||
element.style.animation = 'slideOutRight 0.3s ease-out';
|
||||
setTimeout(() => {
|
||||
if (element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
this.notifications.splice(index, 1);
|
||||
|
||||
// Call dismiss callback
|
||||
if (notification.onDismiss) {
|
||||
notification.onDismiss(notification);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notifications
|
||||
*/
|
||||
clear() {
|
||||
this.notifications.forEach(notification => {
|
||||
this.removeNotification(notification.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification title
|
||||
*/
|
||||
getNotificationTitle(type) {
|
||||
const titles = {
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
warning: 'Warning',
|
||||
info: 'Information'
|
||||
};
|
||||
return titles[type] || 'Notification';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification icon
|
||||
*/
|
||||
getNotificationIcon(type) {
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✗',
|
||||
warning: '⚠',
|
||||
info: 'ℹ'
|
||||
};
|
||||
return icons[type] || 'ℹ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timeout for notification type
|
||||
*/
|
||||
getTimeoutForType(type) {
|
||||
const timeouts = {
|
||||
success: 3000,
|
||||
error: 5000,
|
||||
warning: 4000,
|
||||
info: 3000
|
||||
};
|
||||
return timeouts[type] || 3000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique ID
|
||||
*/
|
||||
generateId() {
|
||||
return 'notification-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading notification
|
||||
*/
|
||||
loading(message, options = {}) {
|
||||
const notification = {
|
||||
id: this.generateId(),
|
||||
type: 'loading',
|
||||
message: message,
|
||||
title: 'Processing',
|
||||
timestamp: Date.now(),
|
||||
autoDismiss: false,
|
||||
persistent: true,
|
||||
actions: [],
|
||||
onDismiss: null
|
||||
};
|
||||
|
||||
this.notifications.push(notification);
|
||||
this.renderLoadingNotification(notification);
|
||||
|
||||
return notification.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render loading notification
|
||||
*/
|
||||
renderLoadingNotification(notification) {
|
||||
const element = document.createElement('div');
|
||||
element.className = `notification notification-loading`;
|
||||
element.dataset.notificationId = notification.id;
|
||||
element.style.cssText = `
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 15px;
|
||||
min-width: 250px;
|
||||
max-width: 400px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
// Spinner
|
||||
const spinner = document.createElement('div');
|
||||
spinner.className = 'notification-spinner';
|
||||
spinner.style.cssText = `
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-top: 2px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
`;
|
||||
|
||||
// Content
|
||||
const content = document.createElement('div');
|
||||
content.className = 'notification-content';
|
||||
content.style.cssText = `
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
`;
|
||||
|
||||
content.textContent = notification.message;
|
||||
|
||||
element.appendChild(spinner);
|
||||
element.appendChild(content);
|
||||
|
||||
this.container.appendChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update loading notification
|
||||
*/
|
||||
updateLoading(id, message) {
|
||||
const element = this.container.querySelector(`[data-notification-id="${id}"]`);
|
||||
if (element) {
|
||||
const content = element.querySelector('.notification-content');
|
||||
if (content) {
|
||||
content.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete loading notification
|
||||
*/
|
||||
completeLoading(id, success = true, message = null) {
|
||||
const element = this.container.querySelector(`[data-notification-id="${id}"]`);
|
||||
if (element) {
|
||||
const type = success ? 'success' : 'error';
|
||||
const title = success ? 'Completed' : 'Failed';
|
||||
const icon = success ? '✓' : '✗';
|
||||
|
||||
// Update content
|
||||
const content = element.querySelector('.notification-content');
|
||||
if (content) {
|
||||
content.textContent = message || (success ? 'Operation completed successfully' : 'Operation failed');
|
||||
}
|
||||
|
||||
// Update styles
|
||||
element.className = `notification notification-${type}`;
|
||||
element.style.borderColor = getComputedStyle(document.documentElement).getPropertyValue(`--${type}-color`);
|
||||
|
||||
// Update spinner to icon
|
||||
const spinner = element.querySelector('.notification-spinner');
|
||||
if (spinner) {
|
||||
spinner.outerHTML = `<div class="notification-icon" style="position: relative; font-size: 18px; color: var(--${type}-color)">${icon}</div>`;
|
||||
}
|
||||
|
||||
// Auto-dismiss after delay
|
||||
setTimeout(() => {
|
||||
this.removeNotification(id);
|
||||
}, success ? 2000 : 4000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show confirmation dialog
|
||||
*/
|
||||
confirm(message, options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const notification = {
|
||||
id: this.generateId(),
|
||||
type: 'confirm',
|
||||
message: message,
|
||||
title: options.title || 'Confirm Action',
|
||||
timestamp: Date.now(),
|
||||
autoDismiss: false,
|
||||
persistent: true,
|
||||
actions: [
|
||||
{
|
||||
label: options.cancelLabel || 'Cancel',
|
||||
onClick: () => resolve(false)
|
||||
},
|
||||
{
|
||||
label: options.confirmLabel || 'Confirm',
|
||||
onClick: () => resolve(true)
|
||||
}
|
||||
],
|
||||
onDismiss: () => resolve(false)
|
||||
};
|
||||
|
||||
this.notifications.push(notification);
|
||||
this.renderNotification(notification);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show prompt dialog
|
||||
*/
|
||||
prompt(message, options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const notification = {
|
||||
id: this.generateId(),
|
||||
type: 'prompt',
|
||||
message: message,
|
||||
title: options.title || 'Input Required',
|
||||
timestamp: Date.now(),
|
||||
autoDismiss: false,
|
||||
persistent: true,
|
||||
actions: [
|
||||
{
|
||||
label: options.cancelLabel || 'Cancel',
|
||||
onClick: () => resolve(null)
|
||||
},
|
||||
{
|
||||
label: options.confirmLabel || 'Submit',
|
||||
onClick: (notification) => {
|
||||
const input = document.querySelector(`[data-notification-id="${notification.id}"] input`);
|
||||
resolve(input ? input.value : null);
|
||||
}
|
||||
}
|
||||
],
|
||||
onDismiss: () => resolve(null)
|
||||
};
|
||||
|
||||
this.notifications.push(notification);
|
||||
this.renderPromptNotification(notification);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render prompt notification
|
||||
*/
|
||||
renderPromptNotification(notification) {
|
||||
const element = document.createElement('div');
|
||||
element.className = `notification notification-prompt`;
|
||||
element.dataset.notificationId = notification.id;
|
||||
element.style.cssText = `
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 15px;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
// Title
|
||||
const title = document.createElement('div');
|
||||
title.className = 'notification-title';
|
||||
title.textContent = notification.title;
|
||||
title.style.cssText = `
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
`;
|
||||
|
||||
// Message
|
||||
const message = document.createElement('div');
|
||||
message.className = 'notification-message';
|
||||
message.textContent = notification.message;
|
||||
message.style.cssText = `
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
`;
|
||||
|
||||
// Input
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.placeholder = 'Enter your response...';
|
||||
input.style.cssText = `
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-secondary);
|
||||
`;
|
||||
|
||||
// Actions
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'notification-actions';
|
||||
actions.style.cssText = `
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
notification.actions.forEach(action => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'btn btn-sm';
|
||||
button.textContent = action.label;
|
||||
button.style.cssText = `
|
||||
font-size: 12px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: ${action.label.toLowerCase().includes('cancel') ? 'transparent' : 'var(--primary-color)'};
|
||||
color: ${action.label.toLowerCase().includes('cancel') ? 'var(--text-secondary)' : 'white'};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (action.onClick) {
|
||||
action.onClick(notification);
|
||||
}
|
||||
this.removeNotification(notification.id);
|
||||
});
|
||||
|
||||
actions.appendChild(button);
|
||||
});
|
||||
|
||||
element.appendChild(title);
|
||||
element.appendChild(message);
|
||||
element.appendChild(input);
|
||||
element.appendChild(actions);
|
||||
|
||||
this.container.appendChild(element);
|
||||
|
||||
// Focus input
|
||||
setTimeout(() => input.focus(), 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification statistics
|
||||
*/
|
||||
getStats() {
|
||||
const stats = {
|
||||
total: this.notifications.length,
|
||||
byType: {},
|
||||
oldest: null,
|
||||
newest: null
|
||||
};
|
||||
|
||||
this.notifications.forEach(notification => {
|
||||
stats.byType[notification.type] = (stats.byType[notification.type] || 0) + 1;
|
||||
});
|
||||
|
||||
if (this.notifications.length > 0) {
|
||||
stats.oldest = this.notifications[0];
|
||||
stats.newest = this.notifications[this.notifications.length - 1];
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export notifications
|
||||
*/
|
||||
exportNotifications() {
|
||||
return {
|
||||
notifications: this.notifications,
|
||||
stats: this.getStats(),
|
||||
exportedAt: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add CSS animations
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.notifications-container {
|
||||
--primary-color: #007bff;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #28a745;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #17a2b8;
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #6c757d;
|
||||
--text-muted: #6c757d;
|
||||
--border-color: #dee2e6;
|
||||
--shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.notification {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.notification:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.notification.notification-success {
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.notification.notification-error {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.notification.notification-warning {
|
||||
border-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.notification.notification-info {
|
||||
border-color: var(--info-color);
|
||||
}
|
||||
|
||||
.notification.notification-loading {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.notification.notification-confirm,
|
||||
.notification.notification-prompt {
|
||||
border-color: var(--info-color);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Notifications;
|
||||
} else {
|
||||
window.Notifications = Notifications;
|
||||
}
|
||||
|
|
@ -0,0 +1,971 @@
|
|||
/**
|
||||
* Settings Module
|
||||
*
|
||||
* Manages application settings, user preferences, and configuration.
|
||||
*/
|
||||
|
||||
class Settings {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.settings = {};
|
||||
this.defaultSettings = {
|
||||
// File Upload Settings
|
||||
fileUpload: {
|
||||
maxFileSize: 50 * 1024 * 1024, // 50MB
|
||||
enableDragDrop: true,
|
||||
enableMultiple: false,
|
||||
previewRows: 100
|
||||
},
|
||||
|
||||
// Data Processing Settings
|
||||
dataProcessing: {
|
||||
defaultMissingValueHandling: 'remove',
|
||||
defaultOutlierHandling: 'remove',
|
||||
outlierThreshold: 3,
|
||||
autoDetectDataTypes: true,
|
||||
enableDataValidation: true,
|
||||
maxPreviewRows: 100,
|
||||
enableCaching: true,
|
||||
cacheTimeout: 300000 // 5 minutes
|
||||
},
|
||||
|
||||
// Visualization Settings
|
||||
visualization: {
|
||||
defaultChartType: 'auto',
|
||||
chartColors: [
|
||||
'#007bff', '#28a745', '#dc3545', '#ffc107', '#17a2b8',
|
||||
'#6f42c1', '#20c997', '#fd7e14', '#e83e8c', '#6c757d'
|
||||
],
|
||||
chartAnimation: true,
|
||||
chartResponsive: true,
|
||||
chartLegendPosition: 'top',
|
||||
chartTooltip: true,
|
||||
chartHeight: 400,
|
||||
chartWidth: 600,
|
||||
enableZoom: true,
|
||||
enablePan: true
|
||||
},
|
||||
|
||||
// Dashboard Settings
|
||||
dashboard: {
|
||||
defaultLayout: 'grid',
|
||||
maxWidgets: 10,
|
||||
enableWidgetResize: true,
|
||||
enableWidgetDrag: true,
|
||||
autoSaveDashboard: true,
|
||||
defaultWidgetSize: {
|
||||
width: 4,
|
||||
height: 3
|
||||
},
|
||||
gridColumns: 12,
|
||||
gridRows: 12,
|
||||
widgetMargin: 10
|
||||
},
|
||||
|
||||
// Export Settings
|
||||
export: {
|
||||
defaultFormat: 'pdf',
|
||||
includeChartsInExport: true,
|
||||
includeDataInExport: true,
|
||||
includeSummaryInExport: true,
|
||||
exportImageQuality: 'high',
|
||||
pdfPageSize: 'A4',
|
||||
pdfOrientation: 'portrait',
|
||||
excelSheetName: 'Processed Data',
|
||||
csvDelimiter: ',',
|
||||
jsonIndent: 2
|
||||
},
|
||||
|
||||
// UI Settings
|
||||
ui: {
|
||||
theme: 'light',
|
||||
language: 'en',
|
||||
enableTooltips: true,
|
||||
enableAnimations: true,
|
||||
fontSize: 'medium',
|
||||
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial',
|
||||
borderRadius: '0.375rem',
|
||||
boxShadow: '0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)',
|
||||
transitionDuration: '0.15s'
|
||||
},
|
||||
|
||||
// Performance Settings
|
||||
performance: {
|
||||
enableVirtualization: true,
|
||||
maxPreviewRows: 100,
|
||||
enableCaching: true,
|
||||
cacheTimeout: 300000, // 5 minutes
|
||||
debounceDelay: 300,
|
||||
throttleDelay: 1000,
|
||||
maxConcurrentOperations: 3
|
||||
},
|
||||
|
||||
// Privacy Settings
|
||||
privacy: {
|
||||
enableAnalytics: false,
|
||||
saveUserPreferences: true,
|
||||
clearDataOnExit: false,
|
||||
storeFileData: false,
|
||||
storeProcessingHistory: false
|
||||
}
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadSettings();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from localStorage
|
||||
*/
|
||||
loadSettings() {
|
||||
try {
|
||||
const savedSettings = localStorage.getItem('excel-analyzer-settings');
|
||||
if (savedSettings) {
|
||||
const parsed = JSON.parse(savedSettings);
|
||||
this.settings = this.mergeSettings(this.defaultSettings, parsed);
|
||||
} else {
|
||||
this.settings = { ...this.defaultSettings };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load settings, using defaults:', error);
|
||||
this.settings = { ...this.defaultSettings };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings to localStorage
|
||||
*/
|
||||
saveSettings() {
|
||||
if (this.settings.privacy.saveUserPreferences) {
|
||||
localStorage.setItem('excel-analyzer-settings', JSON.stringify(this.settings));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get setting value
|
||||
*/
|
||||
get(path, defaultValue = null) {
|
||||
const keys = path.split('.');
|
||||
let current = this.settings;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
current = current[key];
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set setting value
|
||||
*/
|
||||
set(path, value) {
|
||||
const keys = path.split('.');
|
||||
let current = this.settings;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current)) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
|
||||
// Save to localStorage
|
||||
this.saveSettings();
|
||||
|
||||
// Notify change
|
||||
this.notifyChange(path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset settings to defaults
|
||||
*/
|
||||
reset() {
|
||||
this.settings = { ...this.defaultSettings };
|
||||
this.saveSettings();
|
||||
|
||||
// Notify reset
|
||||
this.notifyReset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings
|
||||
*/
|
||||
getAll() {
|
||||
return { ...this.settings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings section
|
||||
*/
|
||||
getSection(section) {
|
||||
return this.settings[section] || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings section
|
||||
*/
|
||||
updateSection(section, values) {
|
||||
if (!this.settings[section]) {
|
||||
this.settings[section] = {};
|
||||
}
|
||||
|
||||
this.settings[section] = { ...this.settings[section], ...values };
|
||||
this.saveSettings();
|
||||
|
||||
// Notify change
|
||||
this.notifyChange(section, this.settings[section]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge settings with defaults
|
||||
*/
|
||||
mergeSettings(defaults, userSettings) {
|
||||
const result = { ...defaults };
|
||||
|
||||
for (const [key, value] of Object.entries(userSettings)) {
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
result[key] = this.mergeSettings(result[key] || {}, value);
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Listen for theme changes
|
||||
window.addEventListener('theme:changed', (event) => {
|
||||
this.set('ui.theme', event.detail.theme);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify setting change
|
||||
*/
|
||||
notifyChange(path, value) {
|
||||
const event = new CustomEvent('settings:changed', {
|
||||
detail: {
|
||||
path: path,
|
||||
value: value,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
// Call callback if provided
|
||||
if (this.options.onSettingChanged) {
|
||||
this.options.onSettingChanged(path, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify settings reset
|
||||
*/
|
||||
notifyReset() {
|
||||
const event = new CustomEvent('settings:reset', {
|
||||
detail: {
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
// Call callback if provided
|
||||
if (this.options.onSettingsReset) {
|
||||
this.options.onSettingsReset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create settings UI
|
||||
*/
|
||||
createSettingsUI(container) {
|
||||
if (!container) return;
|
||||
|
||||
const settingsHTML = `
|
||||
<div class="settings-container">
|
||||
<div class="settings-header">
|
||||
<h4>Application Settings</h4>
|
||||
<div class="settings-actions">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="resetSettingsBtn">
|
||||
<i class="bi bi-arrow-clockwise"></i> Reset to Defaults
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" id="saveSettingsBtn">
|
||||
<i class="bi bi-check"></i> Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-tabs">
|
||||
<ul class="nav nav-tabs" id="settingsTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="general-tab" data-bs-toggle="tab" data-bs-target="#general" type="button">General</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button">Upload</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="processing-tab" data-bs-toggle="tab" data-bs-target="#processing" type="button">Processing</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="visualization-tab" data-bs-toggle="tab" data-bs-target="#visualization" type="button">Visualization</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="export-tab" data-bs-toggle="tab" data-bs-target="#export" type="button">Export</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="privacy-tab" data-bs-toggle="tab" data-bs-target="#privacy" type="button">Privacy</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="settingsTabContent">
|
||||
${this.generateGeneralTab()}
|
||||
${this.generateUploadTab()}
|
||||
${this.generateProcessingTab()}
|
||||
${this.generateVisualizationTab()}
|
||||
${this.generateExportTab()}
|
||||
${this.generatePrivacyTab()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = settingsHTML;
|
||||
|
||||
// Setup tab interactions
|
||||
this.setupSettingsTabs();
|
||||
|
||||
// Setup form interactions
|
||||
this.setupSettingsForms();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate general settings tab
|
||||
*/
|
||||
generateGeneralTab() {
|
||||
const theme = this.get('ui.theme');
|
||||
const language = this.get('ui.language');
|
||||
const fontSize = this.get('ui.fontSize');
|
||||
const enableTooltips = this.get('ui.enableTooltips');
|
||||
const enableAnimations = this.get('ui.enableAnimations');
|
||||
|
||||
return `
|
||||
<div class="tab-pane fade show active" id="general" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Theme</label>
|
||||
<select class="form-select" id="themeSelect">
|
||||
<option value="light" ${theme === 'light' ? 'selected' : ''}>Light</option>
|
||||
<option value="dark" ${theme === 'dark' ? 'selected' : ''}>Dark</option>
|
||||
<option value="blue" ${theme === 'blue' ? 'selected' : ''}>Blue</option>
|
||||
<option value="green" ${theme === 'green' ? 'selected' : ''}>Green</option>
|
||||
<option value="purple" ${theme === 'purple' ? 'selected' : ''}>Purple</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Language</label>
|
||||
<select class="form-select" id="languageSelect">
|
||||
<option value="en" ${language === 'en' ? 'selected' : ''}>English</option>
|
||||
<option value="es" ${language === 'es' ? 'selected' : ''}>Español</option>
|
||||
<option value="fr" ${language === 'fr' ? 'selected' : ''}>Français</option>
|
||||
<option value="de" ${language === 'de' ? 'selected' : ''}>Deutsch</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Font Size</label>
|
||||
<select class="form-select" id="fontSizeSelect">
|
||||
<option value="small" ${fontSize === 'small' ? 'selected' : ''}>Small</option>
|
||||
<option value="medium" ${fontSize === 'medium' ? 'selected' : ''}>Medium</option>
|
||||
<option value="large" ${fontSize === 'large' ? 'selected' : ''}>Large</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableTooltips" ${enableTooltips ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enableTooltips">
|
||||
Enable Tooltips
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableAnimations" ${enableAnimations ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enableAnimations">
|
||||
Enable Animations
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="saveUserPreferences" ${this.get('privacy.saveUserPreferences') ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="saveUserPreferences">
|
||||
Save User Preferences
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="clearDataOnExit" ${this.get('privacy.clearDataOnExit') ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="clearDataOnExit">
|
||||
Clear Data on Exit
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate upload settings tab
|
||||
*/
|
||||
generateUploadTab() {
|
||||
const maxFileSize = this.get('fileUpload.maxFileSize');
|
||||
const enableDragDrop = this.get('fileUpload.enableDragDrop');
|
||||
const enableMultiple = this.get('fileUpload.enableMultiple');
|
||||
const previewRows = this.get('fileUpload.previewRows');
|
||||
|
||||
return `
|
||||
<div class="tab-pane fade" id="upload" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max File Size (MB)</label>
|
||||
<input type="number" class="form-control" id="maxFileSize" value="${maxFileSize / (1024 * 1024)}" min="1" max="100">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Preview Rows</label>
|
||||
<input type="number" class="form-control" id="previewRows" value="${previewRows}" min="10" max="1000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableDragDrop" ${enableDragDrop ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enableDragDrop">
|
||||
Enable Drag & Drop
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableMultiple" ${enableMultiple ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enableMultiple">
|
||||
Enable Multiple Files
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate processing settings tab
|
||||
*/
|
||||
generateProcessingTab() {
|
||||
const autoDetectDataTypes = this.get('dataProcessing.autoDetectDataTypes');
|
||||
const enableDataValidation = this.get('dataProcessing.enableDataValidation');
|
||||
const enableCaching = this.get('dataProcessing.enableCaching');
|
||||
const maxPreviewRows = this.get('dataProcessing.maxPreviewRows');
|
||||
const outlierThreshold = this.get('dataProcessing.outlierThreshold');
|
||||
|
||||
return `
|
||||
<div class="tab-pane fade" id="processing" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Outlier Threshold</label>
|
||||
<input type="number" class="form-control" id="outlierThreshold" value="${outlierThreshold}" min="1" max="10">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Max Preview Rows</label>
|
||||
<input type="number" class="form-control" id="maxPreviewRows" value="${maxPreviewRows}" min="10" max="1000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="autoDetectDataTypes" ${autoDetectDataTypes ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="autoDetectDataTypes">
|
||||
Auto-detect Data Types
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableDataValidation" ${enableDataValidation ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enableDataValidation">
|
||||
Enable Data Validation
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableCaching" ${enableCaching ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enableCaching">
|
||||
Enable Caching
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate visualization settings tab
|
||||
*/
|
||||
generateVisualizationTab() {
|
||||
const defaultChartType = this.get('visualization.defaultChartType');
|
||||
const chartAnimation = this.get('visualization.chartAnimation');
|
||||
const chartResponsive = this.get('visualization.chartResponsive');
|
||||
const chartTooltip = this.get('visualization.chartTooltip');
|
||||
const enableZoom = this.get('visualization.enableZoom');
|
||||
const enablePan = this.get('visualization.enablePan');
|
||||
|
||||
return `
|
||||
<div class="tab-pane fade" id="visualization" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Default Chart Type</label>
|
||||
<select class="form-select" id="defaultChartType">
|
||||
<option value="auto" ${defaultChartType === 'auto' ? 'selected' : ''}>Auto</option>
|
||||
<option value="bar" ${defaultChartType === 'bar' ? 'selected' : ''}>Bar</option>
|
||||
<option value="line" ${defaultChartType === 'line' ? 'selected' : ''}>Line</option>
|
||||
<option value="pie" ${defaultChartType === 'pie' ? 'selected' : ''}>Pie</option>
|
||||
<option value="scatter" ${defaultChartType === 'scatter' ? 'selected' : ''}>Scatter</option>
|
||||
<option value="doughnut" ${defaultChartType === 'doughnut' ? 'selected' : ''}>Doughnut</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Chart Height</label>
|
||||
<input type="number" class="form-control" id="chartHeight" value="${this.get('visualization.chartHeight')}" min="200" max="800">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Chart Width</label>
|
||||
<input type="number" class="form-control" id="chartWidth" value="${this.get('visualization.chartWidth')}" min="400" max="1200">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="chartAnimation" ${chartAnimation ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="chartAnimation">
|
||||
Enable Chart Animation
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="chartResponsive" ${chartResponsive ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="chartResponsive">
|
||||
Enable Responsive Charts
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="chartTooltip" ${chartTooltip ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="chartTooltip">
|
||||
Enable Chart Tooltips
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableZoom" ${enableZoom ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enableZoom">
|
||||
Enable Chart Zoom
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enablePan" ${enablePan ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enablePan">
|
||||
Enable Chart Pan
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate export settings tab
|
||||
*/
|
||||
generateExportTab() {
|
||||
const defaultFormat = this.get('export.defaultFormat');
|
||||
const includeChartsInExport = this.get('export.includeChartsInExport');
|
||||
const includeDataInExport = this.get('export.includeDataInExport');
|
||||
const includeSummaryInExport = this.get('export.includeSummaryInExport');
|
||||
const exportImageQuality = this.get('export.exportImageQuality');
|
||||
|
||||
return `
|
||||
<div class="tab-pane fade" id="export" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Default Export Format</label>
|
||||
<select class="form-select" id="defaultExportFormat">
|
||||
<option value="pdf" ${defaultFormat === 'pdf' ? 'selected' : ''}>PDF</option>
|
||||
<option value="excel" ${defaultFormat === 'excel' ? 'selected' : ''}>Excel</option>
|
||||
<option value="csv" ${defaultFormat === 'csv' ? 'selected' : ''}>CSV</option>
|
||||
<option value="json" ${defaultFormat === 'json' ? 'selected' : ''}>JSON</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Export Image Quality</label>
|
||||
<select class="form-select" id="exportImageQuality">
|
||||
<option value="low" ${exportImageQuality === 'low' ? 'selected' : ''}>Low</option>
|
||||
<option value="medium" ${exportImageQuality === 'medium' ? 'selected' : ''}>Medium</option>
|
||||
<option value="high" ${exportImageQuality === 'high' ? 'selected' : ''}>High</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="includeChartsInExport" ${includeChartsInExport ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="includeChartsInExport">
|
||||
Include Charts in Export
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="includeDataInExport" ${includeDataInExport ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="includeDataInExport">
|
||||
Include Data in Export
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="includeSummaryInExport" ${includeSummaryInExport ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="includeSummaryInExport">
|
||||
Include Summary in Export
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate privacy settings tab
|
||||
*/
|
||||
generatePrivacyTab() {
|
||||
const enableAnalytics = this.get('privacy.enableAnalytics');
|
||||
const saveUserPreferences = this.get('privacy.saveUserPreferences');
|
||||
const clearDataOnExit = this.get('privacy.clearDataOnExit');
|
||||
const storeFileData = this.get('privacy.storeFileData');
|
||||
const storeProcessingHistory = this.get('privacy.storeProcessingHistory');
|
||||
|
||||
return `
|
||||
<div class="tab-pane fade" id="privacy" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableAnalytics" ${enableAnalytics ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="enableAnalytics">
|
||||
Enable Analytics
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="saveUserPreferences" ${saveUserPreferences ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="saveUserPreferences">
|
||||
Save User Preferences
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="clearDataOnExit" ${clearDataOnExit ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="clearDataOnExit">
|
||||
Clear Data on Exit
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="storeFileData" ${storeFileData ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="storeFileData">
|
||||
Store File Data
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="storeProcessingHistory" ${storeProcessingHistory ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="storeProcessingHistory">
|
||||
Store Processing History
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<h6>Privacy Notice</h6>
|
||||
<p>These settings control how your data is stored and processed. Disabling certain features may affect application functionality.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup settings tabs
|
||||
*/
|
||||
setupSettingsTabs() {
|
||||
const tabs = document.querySelectorAll('.nav-link');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('shown.bs.tab', (e) => {
|
||||
// Handle tab-specific logic if needed
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup settings forms
|
||||
*/
|
||||
setupSettingsForms() {
|
||||
// General settings
|
||||
const themeSelect = document.getElementById('themeSelect');
|
||||
if (themeSelect) {
|
||||
themeSelect.addEventListener('change', (e) => {
|
||||
this.set('ui.theme', e.target.value);
|
||||
if (this.options.themeManager) {
|
||||
this.options.themeManager.setTheme(e.target.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
if (languageSelect) {
|
||||
languageSelect.addEventListener('change', (e) => {
|
||||
this.set('ui.language', e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
const fontSizeSelect = document.getElementById('fontSizeSelect');
|
||||
if (fontSizeSelect) {
|
||||
fontSizeSelect.addEventListener('change', (e) => {
|
||||
this.set('ui.fontSize', e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Boolean settings
|
||||
const booleanSettings = [
|
||||
{ id: 'enableTooltips', path: 'ui.enableTooltips' },
|
||||
{ id: 'enableAnimations', path: 'ui.enableAnimations' },
|
||||
{ id: 'enableDragDrop', path: 'fileUpload.enableDragDrop' },
|
||||
{ id: 'enableMultiple', path: 'fileUpload.enableMultiple' },
|
||||
{ id: 'autoDetectDataTypes', path: 'dataProcessing.autoDetectDataTypes' },
|
||||
{ id: 'enableDataValidation', path: 'dataProcessing.enableDataValidation' },
|
||||
{ id: 'enableCaching', path: 'dataProcessing.enableCaching' },
|
||||
{ id: 'chartAnimation', path: 'visualization.chartAnimation' },
|
||||
{ id: 'chartResponsive', path: 'visualization.chartResponsive' },
|
||||
{ id: 'chartTooltip', path: 'visualization.chartTooltip' },
|
||||
{ id: 'enableZoom', path: 'visualization.enableZoom' },
|
||||
{ id: 'enablePan', path: 'visualization.enablePan' },
|
||||
{ id: 'includeChartsInExport', path: 'export.includeChartsInExport' },
|
||||
{ id: 'includeDataInExport', path: 'export.includeDataInExport' },
|
||||
{ id: 'includeSummaryInExport', path: 'export.includeSummaryInExport' },
|
||||
{ id: 'enableAnalytics', path: 'privacy.enableAnalytics' },
|
||||
{ id: 'saveUserPreferences', path: 'privacy.saveUserPreferences' },
|
||||
{ id: 'clearDataOnExit', path: 'privacy.clearDataOnExit' },
|
||||
{ id: 'storeFileData', path: 'privacy.storeFileData' },
|
||||
{ id: 'storeProcessingHistory', path: 'privacy.storeProcessingHistory' }
|
||||
];
|
||||
|
||||
booleanSettings.forEach(setting => {
|
||||
const element = document.getElementById(setting.id);
|
||||
if (element) {
|
||||
element.addEventListener('change', (e) => {
|
||||
this.set(setting.path, e.target.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Number settings
|
||||
const numberSettings = [
|
||||
{ id: 'maxFileSize', path: 'fileUpload.maxFileSize', multiplier: 1024 * 1024 },
|
||||
{ id: 'previewRows', path: 'fileUpload.previewRows' },
|
||||
{ id: 'outlierThreshold', path: 'dataProcessing.outlierThreshold' },
|
||||
{ id: 'maxPreviewRows', path: 'dataProcessing.maxPreviewRows' },
|
||||
{ id: 'chartHeight', path: 'visualization.chartHeight' },
|
||||
{ id: 'chartWidth', path: 'visualization.chartWidth' }
|
||||
];
|
||||
|
||||
numberSettings.forEach(setting => {
|
||||
const element = document.getElementById(setting.id);
|
||||
if (element) {
|
||||
element.addEventListener('change', (e) => {
|
||||
let value = parseInt(e.target.value);
|
||||
if (setting.multiplier) {
|
||||
value = value * setting.multiplier;
|
||||
}
|
||||
this.set(setting.path, value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Select settings
|
||||
const selectSettings = [
|
||||
{ id: 'defaultChartType', path: 'visualization.defaultChartType' },
|
||||
{ id: 'defaultExportFormat', path: 'export.defaultFormat' },
|
||||
{ id: 'exportImageQuality', path: 'export.exportImageQuality' }
|
||||
];
|
||||
|
||||
selectSettings.forEach(setting => {
|
||||
const element = document.getElementById(setting.id);
|
||||
if (element) {
|
||||
element.addEventListener('change', (e) => {
|
||||
this.set(setting.path, e.target.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Reset and save buttons
|
||||
const resetBtn = document.getElementById('resetSettingsBtn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reset all settings to defaults?')) {
|
||||
this.reset();
|
||||
this.updateSettingsUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const saveBtn = document.getElementById('saveSettingsBtn');
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', () => {
|
||||
this.saveSettings();
|
||||
alert('Settings saved successfully!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings UI with current values
|
||||
*/
|
||||
updateSettingsUI() {
|
||||
// This would update all form elements with current setting values
|
||||
// Implementation would depend on the specific UI structure
|
||||
}
|
||||
|
||||
/**
|
||||
* Export settings
|
||||
*/
|
||||
exportSettings() {
|
||||
const settingsData = {
|
||||
settings: this.getAll(),
|
||||
exportedAt: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
};
|
||||
|
||||
return JSON.stringify(settingsData, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import settings
|
||||
*/
|
||||
importSettings(settingsData) {
|
||||
try {
|
||||
const parsed = typeof settingsData === 'string' ? JSON.parse(settingsData) : settingsData;
|
||||
if (parsed.settings) {
|
||||
this.settings = this.mergeSettings(this.defaultSettings, parsed.settings);
|
||||
this.saveSettings();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to import settings:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings statistics
|
||||
*/
|
||||
getSettingsStats() {
|
||||
return {
|
||||
totalSettings: Object.keys(this.settings).length,
|
||||
sections: Object.keys(this.settings),
|
||||
lastModified: localStorage.getItem('excel-analyzer-settings') ?
|
||||
new Date(JSON.parse(localStorage.getItem('excel-analyzer-settings')).timestamp || Date.now()).toISOString() :
|
||||
'Never'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Settings;
|
||||
} else {
|
||||
window.Settings = Settings;
|
||||
}
|
||||
|
|
@ -0,0 +1,591 @@
|
|||
/**
|
||||
* Visualization Module
|
||||
*
|
||||
* Handles chart rendering, interactive visualizations, and chart management.
|
||||
*/
|
||||
|
||||
class Visualizer {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.charts = new Map();
|
||||
this.currentChart = null;
|
||||
this.chartContainer = null;
|
||||
this.chartCanvas = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.chartContainer = document.getElementById('analysisChart');
|
||||
if (!this.chartContainer) {
|
||||
throw new Error('Chart container not found');
|
||||
}
|
||||
|
||||
this.setupChartContainer();
|
||||
}
|
||||
|
||||
setupChartContainer() {
|
||||
// Ensure canvas is properly sized
|
||||
this.chartContainer.style.width = '100%';
|
||||
this.chartContainer.style.height = '400px';
|
||||
|
||||
// Setup responsive behavior
|
||||
this.setupResponsiveChart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render chart
|
||||
*/
|
||||
async renderChart(data, config = {}) {
|
||||
try {
|
||||
// Determine chart type
|
||||
const chartType = this.selectChartType(data, config);
|
||||
|
||||
// Prepare chart data
|
||||
const chartData = this.prepareChartData(data, chartType, config);
|
||||
|
||||
// Prepare chart options
|
||||
const chartOptions = this.prepareChartOptions(chartType, config);
|
||||
|
||||
// Create chart
|
||||
const chart = await this.createChart(chartType, chartData, chartOptions);
|
||||
|
||||
// Store chart
|
||||
this.charts.set(chart.id, chart);
|
||||
this.currentChart = chart;
|
||||
|
||||
// Notify completion
|
||||
if (this.options.onChartRendered) {
|
||||
this.options.onChartRendered(chart);
|
||||
}
|
||||
|
||||
return chart;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chart rendering failed:', error);
|
||||
if (this.options.onError) {
|
||||
this.options.onError(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing chart
|
||||
*/
|
||||
async updateChart(chartId, newData, config = {}) {
|
||||
try {
|
||||
const chart = this.charts.get(chartId);
|
||||
if (!chart) {
|
||||
throw new Error('Chart not found');
|
||||
}
|
||||
|
||||
// Update chart data
|
||||
const chartData = this.prepareChartData(newData, chart.config.type, config);
|
||||
chart.data = chartData;
|
||||
|
||||
// Update chart options if provided
|
||||
if (Object.keys(config).length > 0) {
|
||||
const chartOptions = this.prepareChartOptions(chart.config.type, config);
|
||||
chart.options = { ...chart.options, ...chartOptions };
|
||||
}
|
||||
|
||||
// Update chart
|
||||
chart.update();
|
||||
|
||||
// Notify update
|
||||
if (this.options.onChartUpdated) {
|
||||
this.options.onChartUpdated(chart);
|
||||
}
|
||||
|
||||
return chart;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chart update failed:', error);
|
||||
if (this.options.onError) {
|
||||
this.options.onError(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select chart type based on data
|
||||
*/
|
||||
selectChartType(data, config = {}) {
|
||||
// If explicitly specified, use that
|
||||
if (config.type && config.type !== 'auto') {
|
||||
return config.type;
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return 'bar';
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
const numericColumns = this.getNumericColumns(data);
|
||||
const categoricalColumns = this.getCategoricalColumns(data);
|
||||
|
||||
// Auto-select based on data characteristics
|
||||
if (numericColumns.length >= 2 && categoricalColumns.length === 0) {
|
||||
// Multiple numeric columns - scatter plot or line chart
|
||||
return 'scatter';
|
||||
} else if (numericColumns.length === 1 && categoricalColumns.length >= 1) {
|
||||
// One numeric, multiple categorical - bar chart
|
||||
return 'bar';
|
||||
} else if (numericColumns.length === 1 && categoricalColumns.length === 0) {
|
||||
// Single numeric - pie chart for proportions
|
||||
return 'pie';
|
||||
} else {
|
||||
// Default to bar chart
|
||||
return 'bar';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare chart data
|
||||
*/
|
||||
prepareChartData(data, chartType, config = {}) {
|
||||
const xColumn = config.xAxis || this.getDefaultXAxis(data, chartType);
|
||||
const yColumn = config.yAxis || this.getDefaultYAxis(data, chartType);
|
||||
|
||||
if (chartType === 'pie' || chartType === 'doughnut') {
|
||||
return this.preparePieChartData(data, yColumn);
|
||||
} else if (chartType === 'scatter') {
|
||||
return this.prepareScatterChartData(data, xColumn, yColumn);
|
||||
} else {
|
||||
return this.prepareStandardChartData(data, xColumn, yColumn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare pie chart data
|
||||
*/
|
||||
preparePieChartData(data, valueColumn) {
|
||||
const labels = data.map(row => row[valueColumn]);
|
||||
const values = data.map(row => parseFloat(row[valueColumn]) || 0);
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: values,
|
||||
backgroundColor: this.generateColors(values.length),
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare scatter chart data
|
||||
*/
|
||||
prepareScatterChartData(data, xColumn, yColumn) {
|
||||
const points = data.map(row => ({
|
||||
x: parseFloat(row[xColumn]) || 0,
|
||||
y: parseFloat(row[yColumn]) || 0
|
||||
}));
|
||||
|
||||
return {
|
||||
datasets: [{
|
||||
label: `${xColumn} vs ${yColumn}`,
|
||||
data: points,
|
||||
backgroundColor: this.generateColors(points.length),
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 7
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare standard chart data (bar, line, etc.)
|
||||
*/
|
||||
prepareStandardChartData(data, xColumn, yColumn) {
|
||||
const labels = data.map(row => row[xColumn]);
|
||||
const values = data.map(row => parseFloat(row[yColumn]) || 0);
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: yColumn,
|
||||
data: values,
|
||||
backgroundColor: this.generateColors(values.length),
|
||||
borderColor: this.generateColors(values.length, 0.8),
|
||||
borderWidth: 2,
|
||||
fill: false
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare chart options
|
||||
*/
|
||||
prepareChartOptions(chartType, config = {}) {
|
||||
const theme = this.getCurrentTheme();
|
||||
const colors = this.getThemeColors(theme);
|
||||
|
||||
const baseOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easing: 'easeInOutQuart'
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: config.legendPosition || 'top',
|
||||
labels: {
|
||||
color: colors.text,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
backgroundColor: colors.bgSecondary,
|
||||
titleColor: colors.text,
|
||||
bodyColor: colors.text,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: colors.border
|
||||
},
|
||||
ticks: {
|
||||
color: colors.text
|
||||
}
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
color: colors.border
|
||||
},
|
||||
ticks: {
|
||||
color: colors.text
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Chart-specific options
|
||||
switch (chartType) {
|
||||
case 'bar':
|
||||
return {
|
||||
...baseOptions,
|
||||
indexAxis: config.horizontal ? 'y' : 'x',
|
||||
scales: {
|
||||
...baseOptions.scales,
|
||||
x: {
|
||||
...baseOptions.scales.x,
|
||||
beginAtZero: true
|
||||
},
|
||||
y: {
|
||||
...baseOptions.scales.y,
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
case 'line':
|
||||
return {
|
||||
...baseOptions,
|
||||
tension: 0.4,
|
||||
pointRadius: 3,
|
||||
pointHoverRadius: 5
|
||||
};
|
||||
|
||||
case 'pie':
|
||||
case 'doughnut':
|
||||
return {
|
||||
...baseOptions,
|
||||
cutout: chartType === 'doughnut' ? '50%' : '0%',
|
||||
plugins: {
|
||||
...baseOptions.plugins,
|
||||
legend: {
|
||||
...baseOptions.plugins.legend,
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
case 'scatter':
|
||||
return {
|
||||
...baseOptions,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
position: 'bottom',
|
||||
grid: {
|
||||
color: colors.border
|
||||
},
|
||||
ticks: {
|
||||
color: colors.text
|
||||
}
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
grid: {
|
||||
color: colors.border
|
||||
},
|
||||
ticks: {
|
||||
color: colors.text
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
default:
|
||||
return baseOptions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create chart instance
|
||||
*/
|
||||
async createChart(chartType, data, options) {
|
||||
// Destroy existing chart if any
|
||||
if (this.currentChart) {
|
||||
this.currentChart.destroy();
|
||||
}
|
||||
|
||||
// Create new chart
|
||||
const chart = new Chart(this.chartContainer, {
|
||||
type: chartType,
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize charts for responsiveness
|
||||
*/
|
||||
resizeCharts() {
|
||||
this.charts.forEach(chart => {
|
||||
if (chart && chart.resize) {
|
||||
chart.resize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update chart theme
|
||||
*/
|
||||
updateTheme(theme) {
|
||||
this.charts.forEach(chart => {
|
||||
if (chart) {
|
||||
const colors = this.getThemeColors(theme);
|
||||
chart.options.plugins.legend.labels.color = colors.text;
|
||||
chart.options.plugins.tooltip.backgroundColor = colors.bgSecondary;
|
||||
chart.options.plugins.tooltip.titleColor = colors.text;
|
||||
chart.options.plugins.tooltip.bodyColor = colors.text;
|
||||
chart.options.plugins.tooltip.borderColor = colors.border;
|
||||
chart.options.scales.x.grid.color = colors.border;
|
||||
chart.options.scales.x.ticks.color = colors.text;
|
||||
chart.options.scales.y.grid.color = colors.border;
|
||||
chart.options.scales.y.ticks.color = colors.text;
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Export chart as image
|
||||
*/
|
||||
exportChart(chartId, format = 'png') {
|
||||
const chart = this.charts.get(chartId);
|
||||
if (!chart) {
|
||||
throw new Error('Chart not found');
|
||||
}
|
||||
|
||||
const dataURL = chart.toBase64Image(format, 1.0);
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chart statistics
|
||||
*/
|
||||
getChartStatistics(chartId) {
|
||||
const chart = this.charts.get(chartId);
|
||||
if (!chart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = chart.data.datasets[0].data;
|
||||
const stats = {
|
||||
min: Math.min(...data),
|
||||
max: Math.max(...data),
|
||||
mean: data.reduce((sum, val) => sum + val, 0) / data.length,
|
||||
count: data.length
|
||||
};
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric columns from data
|
||||
*/
|
||||
getNumericColumns(data) {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
return headers.filter(header => {
|
||||
const values = data.map(row => row[header]);
|
||||
return values.every(val => !isNaN(parseFloat(val)));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get categorical columns from data
|
||||
*/
|
||||
getCategoricalColumns(data) {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
return headers.filter(header => {
|
||||
const values = data.map(row => row[header]);
|
||||
return values.some(val => isNaN(parseFloat(val)));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default X axis for chart type
|
||||
*/
|
||||
getDefaultXAxis(data, chartType) {
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
// For scatter plots, prefer numeric columns
|
||||
if (chartType === 'scatter') {
|
||||
const numericColumns = this.getNumericColumns(data);
|
||||
return numericColumns.length > 0 ? numericColumns[0] : headers[0];
|
||||
}
|
||||
|
||||
// For other charts, prefer categorical columns
|
||||
const categoricalColumns = this.getCategoricalColumns(data);
|
||||
return categoricalColumns.length > 0 ? categoricalColumns[0] : headers[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default Y axis for chart type
|
||||
*/
|
||||
getDefaultYAxis(data, chartType) {
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
// Prefer numeric columns for Y axis
|
||||
const numericColumns = this.getNumericColumns(data);
|
||||
return numericColumns.length > 0 ? numericColumns[0] : headers[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate colors for chart
|
||||
*/
|
||||
generateColors(count, alpha = 1.0) {
|
||||
const colors = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hue = (i * 137.508) % 360; // Golden angle
|
||||
colors.push(this.hslToHex(hue, 70, 50));
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HSL to Hex
|
||||
*/
|
||||
hslToHex(h, s, l) {
|
||||
l /= 100;
|
||||
const a = s * Math.min(l, 1 - l) / 100;
|
||||
const f = n => {
|
||||
const k = (n + h / 30) % 12;
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
return Math.round(255 * color).toString(16).padStart(2, '0');
|
||||
};
|
||||
return `#${f(0)}${f(8)}${f(4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme colors
|
||||
*/
|
||||
getThemeColors(theme) {
|
||||
// Get colors from CSS custom properties
|
||||
const root = document.documentElement;
|
||||
return {
|
||||
primary: getComputedStyle(root).getPropertyValue('--primary-color').trim(),
|
||||
secondary: getComputedStyle(root).getPropertyValue('--secondary-color').trim(),
|
||||
success: getComputedStyle(root).getPropertyValue('--success-color').trim(),
|
||||
danger: getComputedStyle(root).getPropertyValue('--danger-color').trim(),
|
||||
warning: getComputedStyle(root).getPropertyValue('--warning-color').trim(),
|
||||
info: getComputedStyle(root).getPropertyValue('--info-color').trim(),
|
||||
text: getComputedStyle(root).getPropertyValue('--text-primary').trim(),
|
||||
bgSecondary: getComputedStyle(root).getPropertyValue('--bg-secondary').trim(),
|
||||
border: getComputedStyle(root).getPropertyValue('--border-color').trim()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme
|
||||
*/
|
||||
getCurrentTheme() {
|
||||
return document.documentElement.getAttribute('data-theme') || 'light';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all charts
|
||||
*/
|
||||
clearCharts() {
|
||||
this.charts.forEach(chart => {
|
||||
if (chart && chart.destroy) {
|
||||
chart.destroy();
|
||||
}
|
||||
});
|
||||
this.charts.clear();
|
||||
this.currentChart = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chart by ID
|
||||
*/
|
||||
getChart(chartId) {
|
||||
return this.charts.get(chartId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all charts
|
||||
*/
|
||||
getAllCharts() {
|
||||
return Array.from(this.charts.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove chart
|
||||
*/
|
||||
removeChart(chartId) {
|
||||
const chart = this.charts.get(chartId);
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
this.charts.delete(chartId);
|
||||
if (this.currentChart && this.currentChart.id === chartId) {
|
||||
this.currentChart = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup responsive chart behavior
|
||||
*/
|
||||
setupResponsiveChart() {
|
||||
// Chart.js handles responsiveness automatically with responsive: true
|
||||
// But we can add additional responsive behavior here if needed
|
||||
window.addEventListener('resize', () => {
|
||||
this.resizeCharts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Visualizer;
|
||||
} else {
|
||||
window.Visualizer = Visualizer;
|
||||
}
|
||||
|
|
@ -0,0 +1,552 @@
|
|||
/**
|
||||
* Formatting Utilities
|
||||
*
|
||||
* Data formatting functions for the Excel Analyzer Web application.
|
||||
*/
|
||||
|
||||
class Formatters {
|
||||
/**
|
||||
* Format number with locale
|
||||
*/
|
||||
formatNumber(num, options = {}) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return options.defaultValue || '0';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
useGrouping: true,
|
||||
locale: 'en-US'
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
return Number(num).toLocaleString(config.locale, {
|
||||
minimumFractionDigits: config.minimumFractionDigits,
|
||||
maximumFractionDigits: config.maximumFractionDigits,
|
||||
useGrouping: config.useGrouping
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency
|
||||
*/
|
||||
formatCurrency(num, options = {}) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return options.defaultValue || '$0.00';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
locale: 'en-US'
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
return Number(num).toLocaleString(config.locale, {
|
||||
style: 'currency',
|
||||
currency: config.currency,
|
||||
minimumFractionDigits: config.minimumFractionDigits,
|
||||
maximumFractionDigits: config.maximumFractionDigits
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage
|
||||
*/
|
||||
formatPercentage(num, options = {}) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return options.defaultValue || '0%';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 2,
|
||||
locale: 'en-US'
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
return Number(num).toLocaleString(config.locale, {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: config.minimumFractionDigits,
|
||||
maximumFractionDigits: config.maximumFractionDigits
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date
|
||||
*/
|
||||
formatDate(date, options = {}) {
|
||||
if (!date) {
|
||||
return options.defaultValue || '';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
locale: 'en-US'
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) {
|
||||
return options.defaultValue || '';
|
||||
}
|
||||
|
||||
return d.toLocaleDateString(config.locale, {
|
||||
year: config.year,
|
||||
month: config.month,
|
||||
day: config.day,
|
||||
hour: config.hour,
|
||||
minute: config.minute,
|
||||
second: config.second
|
||||
});
|
||||
} catch (error) {
|
||||
return options.defaultValue || '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time
|
||||
*/
|
||||
formatTime(date, options = {}) {
|
||||
if (!date) {
|
||||
return options.defaultValue || '';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
locale: 'en-US'
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
try {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) {
|
||||
return options.defaultValue || '';
|
||||
}
|
||||
|
||||
return d.toLocaleTimeString(config.locale, {
|
||||
hour: config.hour,
|
||||
minute: config.minute,
|
||||
second: config.second
|
||||
});
|
||||
} catch (error) {
|
||||
return options.defaultValue || '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size
|
||||
*/
|
||||
formatFileSize(bytes, options = {}) {
|
||||
if (bytes === 0) {
|
||||
return options.defaultValue || '0 Bytes';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
decimals: 2,
|
||||
units: ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
if (bytes === 0) return '0 ' + config.units[0];
|
||||
|
||||
const k = 1024;
|
||||
const dm = config.decimals < 0 ? 0 : config.decimals;
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + config.units[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration
|
||||
*/
|
||||
formatDuration(milliseconds, options = {}) {
|
||||
if (!milliseconds || milliseconds <= 0) {
|
||||
return options.defaultValue || '0s';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
showDays: true,
|
||||
showHours: true,
|
||||
showMinutes: true,
|
||||
showSeconds: true,
|
||||
showMilliseconds: false
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
const ms = milliseconds % 1000;
|
||||
const seconds = Math.floor((milliseconds / 1000) % 60);
|
||||
const minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
|
||||
const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
|
||||
const days = Math.floor(milliseconds / (1000 * 60 * 60 * 24));
|
||||
|
||||
const parts = [];
|
||||
|
||||
if (config.showDays && days > 0) {
|
||||
parts.push(`${days}d`);
|
||||
}
|
||||
|
||||
if (config.showHours && (hours > 0 || parts.length > 0)) {
|
||||
parts.push(`${hours}h`);
|
||||
}
|
||||
|
||||
if (config.showMinutes && (minutes > 0 || parts.length > 0)) {
|
||||
parts.push(`${minutes}m`);
|
||||
}
|
||||
|
||||
if (config.showSeconds && (seconds > 0 || parts.length > 0)) {
|
||||
parts.push(`${seconds}s`);
|
||||
}
|
||||
|
||||
if (config.showMilliseconds && ms > 0) {
|
||||
parts.push(`${ms}ms`);
|
||||
}
|
||||
|
||||
return parts.join(' ') || '0s';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format large numbers with abbreviations
|
||||
*/
|
||||
formatLargeNumber(num, options = {}) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return options.defaultValue || '0';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
decimals: 1,
|
||||
abbreviations: {
|
||||
1e3: 'K',
|
||||
1e6: 'M',
|
||||
1e9: 'B',
|
||||
1e12: 'T'
|
||||
}
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
const absNum = Math.abs(num);
|
||||
|
||||
for (const [threshold, abbr] of Object.entries(config.abbreviations).sort((a, b) => b[0] - a[0])) {
|
||||
if (absNum >= threshold) {
|
||||
const value = num / threshold;
|
||||
const decimals = absNum >= threshold * 10 ? 0 : config.decimals;
|
||||
return value.toFixed(decimals) + abbr;
|
||||
}
|
||||
}
|
||||
|
||||
return this.formatNumber(num, { maximumFractionDigits: config.decimals });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format text case
|
||||
*/
|
||||
formatTextCase(text, format = 'title') {
|
||||
if (!text) return '';
|
||||
|
||||
switch (format.toLowerCase()) {
|
||||
case 'upper':
|
||||
return text.toUpperCase();
|
||||
case 'lower':
|
||||
return text.toLowerCase();
|
||||
case 'title':
|
||||
return text.replace(/\w\S*/g, (txt) => {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
});
|
||||
case 'sentence':
|
||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||
case 'camel':
|
||||
return text.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
|
||||
return index === 0 ? word.toLowerCase() : word.toUpperCase();
|
||||
}).replace(/\s+/g, '');
|
||||
case 'pascal':
|
||||
return text.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => {
|
||||
return word.toUpperCase();
|
||||
}).replace(/\s+/g, '');
|
||||
case 'kebab':
|
||||
return text.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
|
||||
case 'snake':
|
||||
return text.replace(/([a-z])([A-Z])/g, '$1_$2').replace(/\s+/g, '_').toLowerCase();
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format phone number
|
||||
*/
|
||||
formatPhoneNumber(phone, options = {}) {
|
||||
if (!phone) return '';
|
||||
|
||||
const defaultOptions = {
|
||||
format: 'US', // US, International, etc.
|
||||
separator: '-'
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
// Remove all non-digit characters
|
||||
const digits = phone.replace(/\D/g, '');
|
||||
|
||||
if (config.format === 'US') {
|
||||
if (digits.length === 10) {
|
||||
return digits.replace(/(\d{3})(\d{3})(\d{4})/, `($1) $2${config.separator}$3`);
|
||||
} else if (digits.length === 11 && digits[0] === '1') {
|
||||
return digits.replace(/(\d{1})(\d{3})(\d{3})(\d{4})/, `+$1 ($2) $3${config.separator}$4`);
|
||||
}
|
||||
}
|
||||
|
||||
// Default format
|
||||
return digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format postal code
|
||||
*/
|
||||
formatPostalCode(code, options = {}) {
|
||||
if (!code) return '';
|
||||
|
||||
const defaultOptions = {
|
||||
format: 'US' // US, Canada, etc.
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
const cleanCode = code.replace(/\s+/g, '').toUpperCase();
|
||||
|
||||
if (config.format === 'US') {
|
||||
// US ZIP code format: 12345 or 12345-6789
|
||||
if (cleanCode.length === 5) {
|
||||
return cleanCode;
|
||||
} else if (cleanCode.length === 9) {
|
||||
return cleanCode.replace(/(\d{5})(\d{4})/, '$1-$2');
|
||||
}
|
||||
} else if (config.format === 'Canada') {
|
||||
// Canadian postal code format: A1A 1A1
|
||||
if (cleanCode.length === 6) {
|
||||
return cleanCode.replace(/(.{3})(.{3})/, '$1 $2');
|
||||
}
|
||||
}
|
||||
|
||||
return cleanCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage change
|
||||
*/
|
||||
formatPercentageChange(oldValue, newValue, options = {}) {
|
||||
if (oldValue === null || newValue === null || oldValue === 0) {
|
||||
return options.defaultValue || '0%';
|
||||
}
|
||||
|
||||
const change = ((newValue - oldValue) / Math.abs(oldValue)) * 100;
|
||||
const formattedChange = this.formatPercentage(Math.abs(change) / 100, options);
|
||||
|
||||
if (newValue > oldValue) {
|
||||
return `+${formattedChange}`;
|
||||
} else if (newValue < oldValue) {
|
||||
return `-${formattedChange}`;
|
||||
} else {
|
||||
return '0%';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format data size
|
||||
*/
|
||||
formatDataSize(bytes, options = {}) {
|
||||
return this.formatFileSize(bytes, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format memory size
|
||||
*/
|
||||
formatMemorySize(bytes, options = {}) {
|
||||
const defaultOptions = {
|
||||
units: ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
};
|
||||
|
||||
return this.formatFileSize(bytes, { ...defaultOptions, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format speed
|
||||
*/
|
||||
formatSpeed(bytesPerSecond, options = {}) {
|
||||
const defaultOptions = {
|
||||
timeUnit: 's' // s, m, h
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
let unit = '/s';
|
||||
let speed = bytesPerSecond;
|
||||
|
||||
switch (config.timeUnit) {
|
||||
case 'm':
|
||||
speed = bytesPerSecond * 60;
|
||||
unit = '/m';
|
||||
break;
|
||||
case 'h':
|
||||
speed = bytesPerSecond * 3600;
|
||||
unit = '/h';
|
||||
break;
|
||||
}
|
||||
|
||||
return this.formatDataSize(speed, { ...config, defaultValue: '0 B' }) + unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format ratio
|
||||
*/
|
||||
formatRatio(numerator, denominator, options = {}) {
|
||||
if (denominator === 0) {
|
||||
return options.defaultValue || '0:1';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
decimals: 2
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
const ratio = numerator / denominator;
|
||||
return this.formatNumber(ratio, { ...config, defaultValue: '0' }) + ':1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format score
|
||||
*/
|
||||
formatScore(score, maxScore = 100, options = {}) {
|
||||
if (score === null || score === undefined) {
|
||||
return options.defaultValue || '0';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
showPercentage: true,
|
||||
showFraction: false,
|
||||
maxDecimals: 2
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
let result = '';
|
||||
|
||||
if (config.showFraction) {
|
||||
result += `${score}/${maxScore}`;
|
||||
}
|
||||
|
||||
if (config.showPercentage) {
|
||||
const percentage = (score / maxScore) * 100;
|
||||
const formattedPercentage = this.formatNumber(percentage, {
|
||||
maximumFractionDigits: config.maxDecimals
|
||||
});
|
||||
|
||||
if (result) {
|
||||
result += ' ';
|
||||
}
|
||||
result += `(${formattedPercentage}%)`;
|
||||
}
|
||||
|
||||
return result || this.formatNumber(score, { maximumFractionDigits: config.maxDecimals });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format progress
|
||||
*/
|
||||
formatProgress(current, total, options = {}) {
|
||||
if (total === 0) {
|
||||
return options.defaultValue || '0%';
|
||||
}
|
||||
|
||||
const percentage = (current / total) * 100;
|
||||
return this.formatPercentage(percentage / 100, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format version
|
||||
*/
|
||||
formatVersion(version, options = {}) {
|
||||
if (!version) return '';
|
||||
|
||||
const defaultOptions = {
|
||||
padZeros: true,
|
||||
maxParts: 4
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
// Remove all non-numeric and non-dot characters except hyphens
|
||||
const cleanVersion = version.replace(/[^\d.-]/g, '');
|
||||
|
||||
// Split by dots and hyphens
|
||||
const parts = cleanVersion.split(/[.-]/).filter(part => part.length > 0);
|
||||
|
||||
if (config.padZeros) {
|
||||
// Pad each part to at least 2 digits
|
||||
const paddedParts = parts.map(part => part.padStart(2, '0'));
|
||||
return paddedParts.slice(0, config.maxParts).join('.');
|
||||
}
|
||||
|
||||
return parts.slice(0, config.maxParts).join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format scientific notation
|
||||
*/
|
||||
formatScientific(num, options = {}) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return options.defaultValue || '0';
|
||||
}
|
||||
|
||||
const defaultOptions = {
|
||||
precision: 2
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
return Number(num).toExponential(config.precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format boolean
|
||||
*/
|
||||
formatBoolean(value, options = {}) {
|
||||
const defaultOptions = {
|
||||
trueLabel: 'Yes',
|
||||
falseLabel: 'No',
|
||||
nullLabel: 'N/A'
|
||||
};
|
||||
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
if (value === true || value === 'true' || value === 1) {
|
||||
return config.trueLabel;
|
||||
} else if (value === false || value === 'false' || value === 0) {
|
||||
return config.falseLabel;
|
||||
} else {
|
||||
return config.nullLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Formatters;
|
||||
} else {
|
||||
window.Formatters = Formatters;
|
||||
}
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
/**
|
||||
* Helper Utilities
|
||||
*
|
||||
* Common utility functions for the Excel Analyzer Web application.
|
||||
*/
|
||||
|
||||
class Helpers {
|
||||
/**
|
||||
* Deep clone an object
|
||||
*/
|
||||
deepClone(obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return new Date(obj.getTime());
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map(item => this.deepClone(item));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = this.deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return clonedObj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique ID
|
||||
*/
|
||||
generateId(prefix = 'id') {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number with commas and decimal places
|
||||
*/
|
||||
formatNumber(num, decimals = 2) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return Number(num).toLocaleString('en-US', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency
|
||||
*/
|
||||
formatCurrency(num, currency = 'USD', decimals = 2) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return '$0.00';
|
||||
}
|
||||
|
||||
return Number(num).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format percentage
|
||||
*/
|
||||
formatPercentage(num, decimals = 2) {
|
||||
if (num === null || num === undefined || isNaN(num)) {
|
||||
return '0%';
|
||||
}
|
||||
|
||||
return Number(num).toLocaleString('en-US', {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date
|
||||
*/
|
||||
formatDate(date, format = 'YYYY-MM-DD') {
|
||||
if (!date) return '';
|
||||
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function
|
||||
*/
|
||||
debounce(func, wait, immediate = false) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(this, args);
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(this, args);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle function
|
||||
*/
|
||||
throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if value is empty
|
||||
*/
|
||||
isEmpty(value) {
|
||||
if (value === null || value === undefined) return true;
|
||||
if (typeof value === 'string') return value.trim() === '';
|
||||
if (Array.isArray(value)) return value.length === 0;
|
||||
if (typeof value === 'object') return Object.keys(value).length === 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object size in bytes
|
||||
*/
|
||||
getObjectSize(obj) {
|
||||
const objStr = JSON.stringify(obj);
|
||||
return new Blob([objStr]).size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize first letter
|
||||
*/
|
||||
capitalize(str) {
|
||||
if (!str) return '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate string
|
||||
*/
|
||||
truncate(str, maxLength = 50, suffix = '...') {
|
||||
if (!str || str.length <= maxLength) return str;
|
||||
return str.substring(0, maxLength - suffix.length) + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension
|
||||
*/
|
||||
getFileExtension(filename) {
|
||||
if (!filename) return '';
|
||||
return filename.split('.').pop().toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is image
|
||||
*/
|
||||
isImageFile(filename) {
|
||||
const ext = this.getFileExtension(filename);
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg'].includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file is Excel
|
||||
*/
|
||||
isExcelFile(filename) {
|
||||
const ext = this.getFileExtension(filename);
|
||||
return ['xlsx', 'xls'].includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get random color
|
||||
*/
|
||||
getRandomColor() {
|
||||
const letters = '0123456789ABCDEF';
|
||||
let color = '#';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
color += letters[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate color palette
|
||||
*/
|
||||
generateColorPalette(count = 5) {
|
||||
const colors = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hue = (i * 137.508) % 360; // Golden angle
|
||||
colors.push(this.hslToHex(hue, 70, 50));
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HSL to Hex
|
||||
*/
|
||||
hslToHex(h, s, l) {
|
||||
l /= 100;
|
||||
const a = s * Math.min(l, 1 - l) / 100;
|
||||
const f = n => {
|
||||
const k = (n + h / 30) % 12;
|
||||
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
|
||||
};
|
||||
return `#${f(0)}${f(8)}${f(4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle array
|
||||
*/
|
||||
shuffleArray(array) {
|
||||
const arr = [...array];
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique values from array
|
||||
*/
|
||||
getUniqueValues(array) {
|
||||
return [...new Set(array)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Group array by property
|
||||
*/
|
||||
groupBy(array, key) {
|
||||
return array.reduce((groups, item) => {
|
||||
const groupKey = item[key];
|
||||
groups[groupKey] = groups[groupKey] || [];
|
||||
groups[groupKey].push(item);
|
||||
return groups;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort array by property
|
||||
*/
|
||||
sortBy(array, key, direction = 'asc') {
|
||||
return [...array].sort((a, b) => {
|
||||
const aVal = a[key];
|
||||
const bVal = b[key];
|
||||
|
||||
if (aVal < bVal) return direction === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in array of objects
|
||||
*/
|
||||
searchInArray(array, searchTerm, keys) {
|
||||
if (!searchTerm) return array;
|
||||
|
||||
const term = searchTerm.toLowerCase();
|
||||
return array.filter(item => {
|
||||
return keys.some(key => {
|
||||
const value = item[key];
|
||||
return value && value.toString().toLowerCase().includes(term);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file
|
||||
*/
|
||||
downloadFile(content, filename, mimeType = 'text/plain') {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy text to clipboard
|
||||
*/
|
||||
copyToClipboard(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
} finally {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email
|
||||
*/
|
||||
isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate URL
|
||||
*/
|
||||
isValidUrl(string) {
|
||||
try {
|
||||
new URL(string);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL parameters
|
||||
*/
|
||||
getUrlParams(url = window.location.href) {
|
||||
const params = {};
|
||||
const urlObj = new URL(url);
|
||||
urlObj.searchParams.forEach((value, key) => {
|
||||
params[key] = value;
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set URL parameters
|
||||
*/
|
||||
setUrlParams(params) {
|
||||
const url = new URL(window.location.href);
|
||||
Object.keys(params).forEach(key => {
|
||||
if (params[key] !== null && params[key] !== undefined) {
|
||||
url.searchParams.set(key, params[key]);
|
||||
} else {
|
||||
url.searchParams.delete(key);
|
||||
}
|
||||
});
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Helpers;
|
||||
} else {
|
||||
window.Helpers = Helpers;
|
||||
}
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
/**
|
||||
* Validation Utilities
|
||||
*
|
||||
* Input validation functions for the Excel Analyzer Web application.
|
||||
*/
|
||||
|
||||
class Validators {
|
||||
/**
|
||||
* Validate file format
|
||||
*/
|
||||
validateFileFormat(file) {
|
||||
const allowedTypes = [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel'
|
||||
];
|
||||
|
||||
if (!file) {
|
||||
return { isValid: false, error: 'No file selected' };
|
||||
}
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Invalid file type. Please upload an Excel file (.xlsx or .xls)'
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate file size
|
||||
*/
|
||||
validateFileSize(file, maxSize = 50 * 1024 * 1024) {
|
||||
if (!file) {
|
||||
return { isValid: false, error: 'No file selected' };
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
const maxSizeMB = Math.round(maxSize / (1024 * 1024));
|
||||
return {
|
||||
isValid: false,
|
||||
error: `File too large. Maximum file size is ${maxSizeMB}MB`
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email address
|
||||
*/
|
||||
validateEmail(email) {
|
||||
if (!email) {
|
||||
return { isValid: false, error: 'Email is required' };
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return { isValid: false, error: 'Invalid email format' };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate URL
|
||||
*/
|
||||
validateUrl(url) {
|
||||
if (!url) {
|
||||
return { isValid: false, error: 'URL is required' };
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(url);
|
||||
return { isValid: true };
|
||||
} catch (error) {
|
||||
return { isValid: false, error: 'Invalid URL format' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate number range
|
||||
*/
|
||||
validateNumberRange(value, min = null, max = null) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return { isValid: false, error: 'Number is required' };
|
||||
}
|
||||
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) {
|
||||
return { isValid: false, error: 'Value must be a number' };
|
||||
}
|
||||
|
||||
if (min !== null && num < min) {
|
||||
return { isValid: false, error: `Value must be at least ${min}` };
|
||||
}
|
||||
|
||||
if (max !== null && num > max) {
|
||||
return { isValid: false, error: `Value must be at most ${max}` };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate string length
|
||||
*/
|
||||
validateStringLength(str, minLength = 1, maxLength = null) {
|
||||
if (!str) {
|
||||
return { isValid: false, error: 'String is required' };
|
||||
}
|
||||
|
||||
if (str.length < minLength) {
|
||||
return { isValid: false, error: `String must be at least ${minLength} characters long` };
|
||||
}
|
||||
|
||||
if (maxLength && str.length > maxLength) {
|
||||
return { isValid: false, error: `String must be at most ${maxLength} characters long` };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate data structure
|
||||
*/
|
||||
validateDataStructure(data) {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return { isValid: false, error: 'Invalid data structure' };
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return { isValid: false, error: 'No data available' };
|
||||
}
|
||||
|
||||
const headers = Object.keys(data[0]);
|
||||
if (headers.length === 0) {
|
||||
return { isValid: false, error: 'No columns found in data' };
|
||||
}
|
||||
|
||||
// Check for consistent structure
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const rowHeaders = Object.keys(data[i]);
|
||||
if (rowHeaders.length !== headers.length) {
|
||||
return { isValid: false, error: `Inconsistent data structure at row ${i + 1}` };
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate chart configuration
|
||||
*/
|
||||
validateChartConfig(config) {
|
||||
const requiredFields = ['type', 'data'];
|
||||
const errors = [];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (!config[field]) {
|
||||
errors.push(`Missing required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.type && !['bar', 'line', 'pie', 'scatter', 'doughnut'].includes(config.type)) {
|
||||
errors.push('Invalid chart type');
|
||||
}
|
||||
|
||||
if (config.data && !Array.isArray(config.data)) {
|
||||
errors.push('Chart data must be an array');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { isValid: false, error: errors.join(', ') };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate export settings
|
||||
*/
|
||||
validateExportSettings(settings) {
|
||||
const errors = [];
|
||||
|
||||
if (!settings) {
|
||||
return { isValid: false, error: 'Export settings are required' };
|
||||
}
|
||||
|
||||
const validFormats = ['pdf', 'excel', 'csv', 'json'];
|
||||
if (settings.format && !validFormats.includes(settings.format)) {
|
||||
errors.push('Invalid export format');
|
||||
}
|
||||
|
||||
if (settings.includeCharts !== undefined && typeof settings.includeCharts !== 'boolean') {
|
||||
errors.push('includeCharts must be boolean');
|
||||
}
|
||||
|
||||
if (settings.includeData !== undefined && typeof settings.includeData !== 'boolean') {
|
||||
errors.push('includeData must be boolean');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { isValid: false, error: errors.join(', ') };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate dashboard configuration
|
||||
*/
|
||||
validateDashboardConfig(config) {
|
||||
const errors = [];
|
||||
|
||||
if (!config) {
|
||||
return { isValid: false, error: 'Dashboard configuration is required' };
|
||||
}
|
||||
|
||||
if (config.widgets && !Array.isArray(config.widgets)) {
|
||||
errors.push('Widgets must be an array');
|
||||
}
|
||||
|
||||
if (config.layout && typeof config.layout !== 'string') {
|
||||
errors.push('Layout must be a string');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { isValid: false, error: errors.join(', ') };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate settings object
|
||||
*/
|
||||
validateSettings(settings) {
|
||||
const errors = [];
|
||||
|
||||
if (!settings || typeof settings !== 'object') {
|
||||
return { isValid: false, error: 'Settings must be an object' };
|
||||
}
|
||||
|
||||
// Validate specific settings
|
||||
if (settings.maxFileSize && !this.validateNumberRange(settings.maxFileSize, 1024).isValid) {
|
||||
errors.push('maxFileSize must be at least 1024 bytes');
|
||||
}
|
||||
|
||||
if (settings.maxRows && !this.validateNumberRange(settings.maxRows, 1, 100000).isValid) {
|
||||
errors.push('maxRows must be between 1 and 100000');
|
||||
}
|
||||
|
||||
if (settings.maxColumns && !this.validateNumberRange(settings.maxColumns, 1, 1000).isValid) {
|
||||
errors.push('maxColumns must be between 1 and 1000');
|
||||
}
|
||||
|
||||
if (settings.outlierThreshold && !this.validateNumberRange(settings.outlierThreshold, 1).isValid) {
|
||||
errors.push('outlierThreshold must be at least 1');
|
||||
}
|
||||
|
||||
if (settings.theme && !['light', 'dark'].includes(settings.theme)) {
|
||||
errors.push('theme must be either "light" or "dark"');
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { isValid: false, error: errors.join(', ') };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate color value
|
||||
*/
|
||||
validateColor(color) {
|
||||
if (!color) {
|
||||
return { isValid: false, error: 'Color is required' };
|
||||
}
|
||||
|
||||
// Check for hex color
|
||||
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
||||
if (hexRegex.test(color)) {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
// Check for RGB color
|
||||
const rgbRegex = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/;
|
||||
if (rgbRegex.test(color)) {
|
||||
const matches = color.match(rgbRegex);
|
||||
const r = parseInt(matches[1]);
|
||||
const g = parseInt(matches[2]);
|
||||
const b = parseInt(matches[3]);
|
||||
|
||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
||||
return { isValid: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for named colors
|
||||
const namedColors = [
|
||||
'red', 'blue', 'green', 'yellow', 'orange', 'purple', 'pink', 'brown',
|
||||
'black', 'white', 'gray', 'grey', 'cyan', 'magenta', 'lime', 'navy',
|
||||
'maroon', 'olive', 'teal', 'silver', 'aqua', 'fuchsia'
|
||||
];
|
||||
|
||||
if (namedColors.includes(color.toLowerCase())) {
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
return { isValid: false, error: 'Invalid color format' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate date range
|
||||
*/
|
||||
validateDateRange(startDate, endDate) {
|
||||
if (!startDate || !endDate) {
|
||||
return { isValid: false, error: 'Both start and end dates are required' };
|
||||
}
|
||||
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||
return { isValid: false, error: 'Invalid date format' };
|
||||
}
|
||||
|
||||
if (start > end) {
|
||||
return { isValid: false, error: 'Start date must be before end date' };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate array of items
|
||||
*/
|
||||
validateArray(items, validatorFn, minItems = 0, maxItems = null) {
|
||||
if (!Array.isArray(items)) {
|
||||
return { isValid: false, error: 'Must be an array' };
|
||||
}
|
||||
|
||||
if (items.length < minItems) {
|
||||
return { isValid: false, error: `Must have at least ${minItems} items` };
|
||||
}
|
||||
|
||||
if (maxItems && items.length > maxItems) {
|
||||
return { isValid: false, error: `Must have at most ${maxItems} items` };
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const result = validatorFn(items[i]);
|
||||
if (!result.isValid) {
|
||||
return { isValid: false, error: `Item ${i + 1}: ${result.error}` };
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate object properties
|
||||
*/
|
||||
validateObject(obj, rules) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return { isValid: false, error: 'Must be an object' };
|
||||
}
|
||||
|
||||
for (const [key, validator] of Object.entries(rules)) {
|
||||
if (obj[key] !== undefined) {
|
||||
const result = validator(obj[key]);
|
||||
if (!result.isValid) {
|
||||
return { isValid: false, error: `${key}: ${result.error}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required field
|
||||
*/
|
||||
validateRequired(value, fieldName = 'Field') {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return { isValid: false, error: `${fieldName} is required` };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate against custom function
|
||||
*/
|
||||
validateCustom(value, validatorFn, errorMessage = 'Validation failed') {
|
||||
try {
|
||||
const result = validatorFn(value);
|
||||
if (result === true) {
|
||||
return { isValid: true };
|
||||
} else if (typeof result === 'string') {
|
||||
return { isValid: false, error: result };
|
||||
} else {
|
||||
return { isValid: false, error: errorMessage };
|
||||
}
|
||||
} catch (error) {
|
||||
return { isValid: false, error: error.message || errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch validation
|
||||
*/
|
||||
validateBatch(validations) {
|
||||
const results = [];
|
||||
let isValid = true;
|
||||
|
||||
for (const validation of validations) {
|
||||
const result = validation.validator(validation.value, ...validation.args);
|
||||
results.push({
|
||||
...result,
|
||||
field: validation.field
|
||||
});
|
||||
|
||||
if (!result.isValid) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid,
|
||||
results,
|
||||
errors: results.filter(r => !r.isValid).map(r => `${r.field}: ${r.error}`)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in main.js
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Validators;
|
||||
} else {
|
||||
window.Validators = Validators;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue