This commit is contained in:
Getasew 2026-04-09 10:15:17 -06:00 committed by GitHub
commit b9b77cb334
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 10102 additions and 0 deletions

View 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.

View 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
excel-analyzer/
├── 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`

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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;
}
}

View file

@ -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;
}

View 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>&copy; 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>

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View 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
};

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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 = '&times;';
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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}