Weekend Project: Building a Data-Driven Application with Complex Asynchronous Operations

Applying Polya's Problem-Solving Process to Asynchronous JavaScript Challenges

Introduction to the Weekend Project

Throughout this week, we've explored advanced asynchronous JavaScript concepts including error handling strategies, concurrent operations management, and async generators. Now it's time to apply these skills to build a real-world data-driven application that handles complex asynchronous operations.

We'll use George Polya's famous 4-step problem-solving framework to guide our development process. Polya, a renowned mathematician, outlined this structured approach in his book "How to Solve It" (1945), and it applies remarkably well to software development challenges.

flowchart TD A[Polya's Problem Solving Process] --> B[1. Understand the Problem] A --> C[2. Devise a Plan] A --> D[3. Carry Out the Plan] A --> E[4. Look Back & Reflect] B --> B1[Define requirements] B --> B2[Clarify constraints] B --> B3[Identify inputs & outputs] C --> C1[Break down into subproblems] C --> C2[Choose appropriate patterns] C --> C3[Design architecture] D --> D1[Implement solutions] D --> D2[Test & debug] D --> D3[Integrate components] E --> E1[Optimize performance] E --> E2[Refactor & improve] E --> E3[Document lessons learned]

Project Overview: Real-Time Data Dashboard

For this weekend project, you'll build a real-time data dashboard that analyzes and visualizes streaming financial data. The application will fetch data from multiple sources, process it asynchronously, and present it to users in an interactive interface.

Key Features

flowchart LR A[User Interface] <--> B[Application Core] B <--> C[Data Sources] subgraph "User Interface" A1[Dashboard] A2[Visualizations] A3[Controls] end subgraph "Application Core" B1[Data Manager] B2[Stream Processor] B3[Error Handler] end subgraph "Data Sources" C1[Stock API] C2[News WebSocket] C3[Historical Database] end

Step 1: Understand the Problem

The first step in Polya's process is to thoroughly understand the problem. Let's break down the requirements and constraints of our real-time dashboard.

Functional Requirements

Technical Constraints

Data Sources

For this project, we'll use a combination of freely available APIs and simulated data sources:

Understanding the Problem - Key Questions:

  • What are we trying to achieve? (A real-time financial dashboard)
  • What data do we need to present? (Stocks, news, indicators)
  • What are the main challenges? (Async data handling, error resilience, performance)
  • What tools and techniques will help us? (Async generators, Promise combinators, etc.)

Step 2: Devise a Plan

With a clear understanding of the problem, it's time to devise a plan. We'll break the problem into smaller, manageable components and design the architecture of our application.

System Architecture

Our data dashboard will follow a modular architecture with these main components:

classDiagram class DataSourceManager { +addSource(source) +removeSource(id) +connect() +disconnect() } class DataSource { <> +id: string +connect() +disconnect() +stream(): AsyncGenerator } class DataProcessor { +registerTransformation(name, fn) +process(data) } class VisualizationManager { +registerChart(id, config) +updateChart(id, data) +removeChart(id) } class ErrorHandler { +registerStrategy(errorType, strategy) +handleError(error) } class DashboardController { +init() +start() +stop() +addWidget(config) +removeWidget(id) } DataSourceManager -- DataSource : manages DashboardController -- DataSourceManager : uses DashboardController -- DataProcessor : uses DashboardController -- VisualizationManager : uses DashboardController -- ErrorHandler : uses

Key Design Patterns

We'll leverage several design patterns to manage the complexity of our application:

Implementation Plan

Let's break down our implementation plan into smaller tasks:

1. Core Infrastructure

  • Set up project structure and dependencies
  • Implement core utility functions for async operations
  • Create base classes for data sources and processors

2. Data Acquisition

  • Implement REST API client with error handling and retries
  • Create WebSocket client for real-time updates
  • Build SSE client for streaming data
  • Develop data source manager to coordinate all sources

3. Data Processing

  • Create data transformation pipeline
  • Implement financial calculations (moving averages, etc.)
  • Build data filtering and aggregation functions
  • Develop caching strategies for performance

4. Visualization Layer

  • Set up charting library (Chart.js, D3.js, etc.)
  • Create reusable chart components
  • Implement real-time data binding
  • Design responsive layout for the dashboard

5. User Interaction

  • Implement user preference storage
  • Create controls for filtering and customization
  • Add drag-and-drop functionality for dashboard widgets
  • Build settings and configuration panels

6. Error Handling & Resilience

  • Implement centralized error handling system
  • Create recovery strategies for different error types
  • Add logging and monitoring capabilities
  • Design user-friendly error messages and feedback

7. Testing & Optimization

  • Write tests for critical components
  • Optimize performance bottlenecks
  • Implement load testing for concurrent operations
  • Fine-tune memory management

Step 3: Carry Out the Plan

Now it's time to implement our plan. We'll focus on the core components that deal with asynchronous operations, which are central to this week's learning objectives.

Core Infrastructure

Let's start with the essential utility classes for async operations:

Async Utilities


// utils/async.js - Core async utilities

/**
 * Retry an async function with exponential backoff
 * @param {Function} fn - Function to retry
 * @param {Object} options - Retry options
 * @returns {Promise} - Result of the function
 */
export async function retry(fn, options = {}) {
  const { 
    maxRetries = 3, 
    initialDelay = 1000, 
    maxDelay = 30000,
    factor = 2,
    shouldRetry = (error) => true 
  } = options;
  
  let lastError;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn(attempt);
    } catch (error) {
      console.warn(`Attempt ${attempt + 1} failed:`, error);
      lastError = error;
      
      if (attempt >= maxRetries || !shouldRetry(error)) {
        break;
      }
      
      const delay = Math.min(
        maxDelay,
        initialDelay * Math.pow(factor, attempt) * (0.75 + Math.random() * 0.5)
      );
      
      console.log(`Retrying in ${Math.round(delay)}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

/**
 * Execute an async function with a timeout
 * @param {Promise} promise - Promise to add timeout to
 * @param {number} timeoutMs - Timeout in milliseconds
 * @param {string} message - Error message on timeout
 * @returns {Promise} - Promise with timeout
 */
export function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      reject(new Error(message));
    }, timeoutMs);
    
    promise
      .then(result => {
        clearTimeout(timeoutId);
        resolve(result);
      })
      .catch(error => {
        clearTimeout(timeoutId);
        reject(error);
      });
  });
}

/**
 * Limit concurrency of async operations
 * @param {Function[]} tasks - Array of async functions
 * @param {number} concurrency - Maximum number of concurrent tasks
 * @returns {Promise} - Results of all tasks
 */
export async function limitConcurrency(tasks, concurrency = 5) {
  const results = [];
  const executing = new Set();
  
  async function executeTask(task, index) {
    executing.add(task);
    try {
      const result = await task();
      results[index] = { status: 'fulfilled', value: result };
    } catch (error) {
      results[index] = { status: 'rejected', reason: error };
    } finally {
      executing.delete(task);
    }
  }
  
  let index = 0;
  
  // Initial batch of tasks
  while (index < concurrency && index < tasks.length) {
    executeTask(tasks[index], index);
    index++;
  }
  
  // Execute remaining tasks as others complete
  while (index < tasks.length) {
    await Promise.race([...executing].map(task => 
      task.then ? task : Promise.resolve()
    ));
    
    if (executing.size < concurrency) {
      executeTask(tasks[index], index);
      index++;
    }
  }
  
  // Wait for all tasks to complete
  if (executing.size > 0) {
    await Promise.all([...executing].map(task => 
      task.then ? task : Promise.resolve()
    ));
  }
  
  return results;
}

/**
 * Creates a rate limiter
 * @param {number} maxRequests - Maximum requests per interval
 * @param {number} interval - Interval in milliseconds
 * @returns {Function} - Rate limited function wrapper
 */
export function createRateLimiter(maxRequests, interval) {
  const queue = [];
  let tokens = maxRequests;
  let lastRefill = Date.now();
  
  // Refill tokens based on elapsed time
  function refillTokens() {
    const now = Date.now();
    const elapsed = now - lastRefill;
    
    if (elapsed >= interval) {
      const refillAmount = Math.floor(elapsed / interval);
      tokens = Math.min(maxRequests, tokens + refillAmount * maxRequests);
      lastRefill = now;
    }
  }
  
  // Process the queue when tokens are available
  function processQueue() {
    refillTokens();
    
    while (tokens > 0 && queue.length > 0) {
      const { resolve } = queue.shift();
      tokens--;
      resolve();
    }
    
    if (queue.length > 0) {
      const timeToNextRefill = interval - (Date.now() - lastRefill);
      setTimeout(processQueue, timeToNextRefill + 10);
    }
  }
  
  // Acquire a token, returning a promise that resolves when ready
  async function acquireToken() {
    refillTokens();
    
    if (tokens > 0 && queue.length === 0) {
      tokens--;
      return Promise.resolve();
    }
    
    return new Promise(resolve => {
      queue.push({ resolve });
      
      if (queue.length === 1) {
        const timeToNextRefill = interval - (Date.now() - lastRefill);
        setTimeout(processQueue, timeToNextRefill + 10);
      }
    });
  }
  
  // Wrap a function with rate limiting
  return function limitRate(fn) {
    return async function(...args) {
      await acquireToken();
      return fn(...args);
    };
  };
}
          

Error Handling System

Next, let's implement our robust error handling system:

Error Handling Classes


// errors/index.js - Custom error classes and handler

// Base application error
export class AppError extends Error {
  constructor(message, options = {}) {
    super(message);
    this.name = this.constructor.name;
    this.code = options.code || 'APP_ERROR';
    this.status = options.status || 500;
    this.retryable = options.retryable ?? false;
    this.cause = options.cause; // Original error that caused this one
    
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
}

// Network related errors
export class NetworkError extends AppError {
  constructor(message, options = {}) {
    super(message, {
      code: 'NETWORK_ERROR',
      status: 503,
      retryable: true, // Network errors are generally retryable
      ...options
    });
  }
}

// API related errors
export class ApiError extends AppError {
  constructor(message, options = {}) {
    super(message, {
      code: 'API_ERROR',
      ...options
    });
    this.endpoint = options.endpoint;
    this.params = options.params;
  }
}

// Rate limiting errors
export class RateLimitError extends ApiError {
  constructor(message, options = {}) {
    super(message, {
      code: 'RATE_LIMIT_ERROR',
      status: 429,
      retryable: true,
      ...options
    });
    this.retryAfter = options.retryAfter; // Seconds to wait before retry
  }
}

// Data processing errors
export class DataError extends AppError {
  constructor(message, options = {}) {
    super(message, {
      code: 'DATA_ERROR',
      ...options
    });
    this.data = options.data;
  }
}

// Timeout errors
export class TimeoutError extends AppError {
  constructor(message, options = {}) {
    super(message, {
      code: 'TIMEOUT_ERROR',
      status: 408,
      retryable: true,
      ...options
    });
    this.timeoutMs = options.timeoutMs;
  }
}

// Error handler class
export class ErrorHandler {
  constructor() {
    this.strategies = new Map();
    this.defaultStrategy = this.defaultErrorStrategy.bind(this);
    
    // Register common error strategies
    this.registerStrategy('NetworkError', this.handleNetworkError.bind(this));
    this.registerStrategy('RateLimitError', this.handleRateLimitError.bind(this));
    this.registerStrategy('TimeoutError', this.handleTimeoutError.bind(this));
  }
  
  // Register a new error handling strategy
  registerStrategy(errorType, strategyFn) {
    this.strategies.set(errorType, strategyFn);
  }
  
  // Handle an error using the appropriate strategy
  async handleError(error, context = {}) {
    console.error('Handling error:', error);
    
    // Find the appropriate strategy
    const errorName = error.name || error.constructor.name;
    const strategy = this.strategies.get(errorName) || this.defaultStrategy;
    
    try {
      // Apply the strategy
      return await strategy(error, context);
    } catch (strategyError) {
      console.error('Error handling strategy failed:', strategyError);
      
      // Last resort - return a generic error response
      return {
        success: false,
        error: 'A system error occurred. Please try again later.',
        originalError: error.message,
        code: error.code || 'UNKNOWN_ERROR'
      };
    }
  }
  
  // Default error strategy
  async defaultErrorStrategy(error, context) {
    return {
      success: false,
      error: error.message,
      code: error.code || 'UNKNOWN_ERROR'
    };
  }
  
  // Handle network errors
  async handleNetworkError(error, context) {
    // Log additional info
    console.warn('Network error details:', {
      retryable: error.retryable,
      endpoint: context.endpoint,
      attempt: context.attempt
    });
    
    // If retryable and we have retry configuration
    if (error.retryable && context.retry) {
      const { maxRetries, currentRetry } = context.retry;
      
      if (currentRetry < maxRetries) {
        return {
          success: false,
          retry: true,
          error: error.message,
          code: error.code
        };
      }
    }
    
    return {
      success: false,
      error: 'Network connection issue. Please check your connection and try again.',
      code: error.code,
      offline: navigator.onLine === false
    };
  }
  
  // Handle rate limit errors
  async handleRateLimitError(error, context) {
    const retryAfter = error.retryAfter || 60; // Default 1 minute
    
    console.warn(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
    
    // If we're in a UI context, show a message
    if (context.ui) {
      context.ui.showMessage(`API rate limit reached. Retrying in ${retryAfter} seconds...`);
    }
    
    return {
      success: false,
      retry: true,
      retryAfter: retryAfter * 1000, // Convert to milliseconds
      error: 'Rate limit exceeded. Please wait before trying again.',
      code: error.code
    };
  }
  
  // Handle timeout errors
  async handleTimeoutError(error, context) {
    console.warn('Operation timed out:', context);
    
    return {
      success: false,
      error: 'The operation took too long to complete. Please try again.',
      code: error.code,
      timeout: true
    };
  }
}
          

Data Source Management

Let's implement our data sources using async generators:

Data Source Implementations


// data/sources.js - Data source implementations

import { retry, withTimeout, createRateLimiter } from '../utils/async.js';
import { 
  NetworkError, ApiError, RateLimitError, TimeoutError 
} from '../errors/index.js';

// Base data source class
export class DataSource {
  constructor(id, options = {}) {
    this.id = id;
    this.options = options;
    this.connected = false;
    this.errorHandler = options.errorHandler;
  }
  
  // Abstract methods to be implemented by subclasses
  async connect() {
    throw new Error('connect() must be implemented by subclass');
  }
  
  async disconnect() {
    throw new Error('disconnect() must be implemented by subclass');
  }
  
  async *stream() {
    throw new Error('stream() must be implemented by subclass');
  }
  
  // Helper method for handling errors
  async handleError(error, context = {}) {
    if (this.errorHandler) {
      return this.errorHandler.handleError(error, {
        source: this.id,
        ...context
      });
    }
    
    // Default error handling
    console.error(`Error in data source ${this.id}:`, error);
    throw error;
  }
}

// REST API data source
export class RestApiSource extends DataSource {
  constructor(id, endpoint, options = {}) {
    super(id, options);
    this.endpoint = endpoint;
    this.interval = options.interval || 60000; // Default polling interval: 1 minute
    this.timeout = options.timeout || 10000; // Default timeout: 10 seconds
    this.pageSize = options.pageSize || 100;
    this.maxRetries = options.maxRetries || 3;
    
    // Create rate-limited fetch function if API has rate limits
    if (options.rateLimit) {
      const { maxRequests, interval } = options.rateLimit;
      const limiter = createRateLimiter(maxRequests, interval);
      this.fetchWithRateLimit = limiter(this.fetchData.bind(this));
    } else {
      this.fetchWithRateLimit = this.fetchData.bind(this);
    }
    
    this.abortController = new AbortController();
    this.polling = null;
  }
  
  async connect() {
    if (this.connected) return;
    
    // Reset abort controller
    this.abortController = new AbortController();
    
    try {
      // Make an initial request to validate connection
      await this.fetchWithRateLimit();
      this.connected = true;
      console.log(`Connected to API: ${this.endpoint}`);
    } catch (error) {
      await this.handleError(error, { operation: 'connect' });
      throw error;
    }
  }
  
  async disconnect() {
    if (!this.connected) return;
    
    // Abort any pending requests
    this.abortController.abort();
    
    // Clear polling interval
    if (this.polling) {
      clearInterval(this.polling);
      this.polling = null;
    }
    
    this.connected = false;
    console.log(`Disconnected from API: ${this.endpoint}`);
  }
  
  async fetchData(path = '', params = {}) {
    const url = new URL(path, this.endpoint);
    
    // Add query parameters
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined) {
        url.searchParams.append(key, value);
      }
    });
    
    try {
      // Fetch with timeout and abort signal
      const response = await withTimeout(
        fetch(url.toString(), {
          signal: this.abortController.signal,
          headers: this.options.headers
        }),
        this.timeout,
        `Request to ${url} timed out after ${this.timeout}ms`
      );
      
      // Check for rate limiting
      if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After');
        throw new RateLimitError(
          `Rate limit exceeded for ${url}`,
          {
            endpoint: url.toString(),
            retryAfter: retryAfter ? parseInt(retryAfter, 10) : 60
          }
        );
      }
      
      // Check for other HTTP errors
      if (!response.ok) {
        throw new ApiError(
          `API request failed with status ${response.status}`,
          {
            status: response.status,
            endpoint: url.toString(),
            retryable: response.status >= 500
          }
        );
      }
      
      // Parse JSON response
      return await response.json();
    } catch (error) {
      // Convert fetch errors to our custom error types
      if (error.name === 'AbortError') {
        throw new TimeoutError(
          `Request to ${url} was aborted`,
          { endpoint: url.toString() }
        );
      } else if (error instanceof TypeError && error.message.includes('fetch')) {
        throw new NetworkError(
          `Network error while fetching ${url}`,
          { endpoint: url.toString() }
        );
      }
      
      // Re-throw custom errors
      throw error;
    }
  }
  
  // Create an async generator for polling the API
  async *stream() {
    try {
      await this.connect();
      
      let lastFetch = 0;
      
      while (this.connected) {
        const now = Date.now();
        const timeToWait = Math.max(0, this.interval - (now - lastFetch));
        
        if (timeToWait > 0) {
          await new Promise(resolve => setTimeout(resolve, timeToWait));
        }
        
        try {
          const data = await retry(
            () => this.fetchWithRateLimit(),
            { maxRetries: this.maxRetries }
          );
          
          lastFetch = Date.now();
          yield { timestamp: lastFetch, data };
        } catch (error) {
          console.error(`Error fetching data from ${this.endpoint}:`, error);
          
          // Handle error but continue streaming
          const errorResult = await this.handleError(error, { operation: 'fetch' });
          
          if (errorResult.retry && errorResult.retryAfter) {
            console.log(`Waiting ${errorResult.retryAfter}ms before retry...`);
            await new Promise(resolve => setTimeout(resolve, errorResult.retryAfter));
          } else {
            // Non-retryable error, wait the normal interval
            await new Promise(resolve => setTimeout(resolve, this.interval));
          }
        }
      }
    } finally {
      console.log(`Stream ended for ${this.id}`);
      this.disconnect();
    }
  }
  
  // Method for paginated data retrieval
  async *streamPaginated(path = '', params = {}) {
    let page = 1;
    let hasMore = true;
    
    while (hasMore && this.connected) {
      try {
        const data = await retry(
          () => this.fetchWithRateLimit(path, {
            ...params,
            page,
            limit: this.pageSize
          }),
          { maxRetries: this.maxRetries }
        );
        
        // Yield the current page of data
        yield {
          timestamp: Date.now(),
          data: data.items || data.results || data,
          page
        };
        
        // Check if there are more pages
        hasMore = data.hasMore || (data.totalPages && page < data.totalPages);
        page++;
        
        // Don't hammer the API, wait a bit between pages
        await new Promise(resolve => setTimeout(resolve, 1000));
      } catch (error) {
        console.error(`Error fetching page ${page} from ${this.endpoint}:`, error);
        
        // Handle error but continue streaming if possible
        const errorResult = await this.handleError(error, { 
          operation: 'fetchPage',
          page
        });
        
        if (errorResult.retry) {
          // Retry after suggested delay or default
          const delay = errorResult.retryAfter || 5000;
          await new Promise(resolve => setTimeout(resolve, delay));
        } else {
          // Non-retryable error, stop pagination
          hasMore = false;
        }
      }
    }
  }
}

// WebSocket data source
export class WebSocketSource extends DataSource {
  constructor(id, url, options = {}) {
    super(id, options);
    this.url = url;
    this.socket = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectDelay = options.reconnectDelay || 1000;
    this.reconnectBackoffFactor = options.reconnectBackoffFactor || 1.5;
    this.messageQueue = [];
    this.resolveNextMessage = null;
  }
  
  async connect() {
    if (this.connected) return;
    
    this.reconnectAttempts = 0;
    return this.attemptConnect();
  }
  
  async attemptConnect() {
    try {
      // Create WebSocket
      this.socket = new WebSocket(this.url);
      
      // Set up event handlers
      this.socket.addEventListener('open', this.handleOpen.bind(this));
      this.socket.addEventListener('message', this.handleMessage.bind(this));
      this.socket.addEventListener('error', this.handleError.bind(this));
      this.socket.addEventListener('close', this.handleClose.bind(this));
      
      // Wait for connection
      await new Promise((resolve, reject) => {
        const onOpen = () => {
          this.socket.removeEventListener('open', onOpen);
          this.socket.removeEventListener('error', onError);
          resolve();
        };
        
        const onError = (error) => {
          this.socket.removeEventListener('open', onOpen);
          this.socket.removeEventListener('error', onError);
          reject(new NetworkError(`Failed to connect to WebSocket ${this.url}`, {
            retryable: true,
            cause: error
          }));
        };
        
        this.socket.addEventListener('open', onOpen);
        this.socket.addEventListener('error', onError);
      });
      
      this.connected = true;
      this.reconnectAttempts = 0;
      console.log(`Connected to WebSocket: ${this.url}`);
    } catch (error) {
      console.error(`WebSocket connection error (${this.url}):`, error);
      
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        // Calculate backoff delay
        const delay = this.reconnectDelay * 
          Math.pow(this.reconnectBackoffFactor, this.reconnectAttempts);
        
        console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`);
        
        await new Promise(resolve => setTimeout(resolve, delay));
        this.reconnectAttempts++;
        
        // Try to connect again
        return this.attemptConnect();
      }
      
      // Exceeded max reconnect attempts
      await this.handleError(error, { operation: 'connect' });
      throw error;
    }
  }
  
  async disconnect() {
    if (!this.connected || !this.socket) return;
    
    try {
      this.socket.close();
    } catch (error) {
      console.error(`Error closing WebSocket (${this.url}):`, error);
    }
    
    this.socket = null;
    this.connected = false;
    this.messageQueue = [];
    
    if (this.resolveNextMessage) {
      this.resolveNextMessage(null);
      this.resolveNextMessage = null;
    }
    
    console.log(`Disconnected from WebSocket: ${this.url}`);
  }
  
  handleOpen(event) {
    console.log(`WebSocket connection established (${this.url})`);
    
    // Send any authentication or initialization messages
    if (this.options.onConnect) {
      const message = this.options.onConnect();
      if (message) {
        this.socket.send(typeof message === 'string' ? message : JSON.stringify(message));
      }
    }
  }
  
  handleMessage(event) {
    try {
      // Parse message data
      const data = JSON.parse(event.data);
      
      // If we have a waiting resolver, resolve with this message
      if (this.resolveNextMessage) {
        const resolve = this.resolveNextMessage;
        this.resolveNextMessage = null;
        resolve({ timestamp: Date.now(), data });
      } else {
        // Otherwise, queue the message
        this.messageQueue.push({ timestamp: Date.now(), data });
      }
    } catch (error) {
      console.error(`Error parsing WebSocket message (${this.url}):`, error);
    }
  }
  
  async handleError(error, context = {}) {
    console.error(`WebSocket error (${this.url}):`, error);
    
    // Handle error through error handler if available
    if (this.errorHandler) {
      return this.errorHandler.handleError(
        new NetworkError(`WebSocket error: ${error.message}`, {
          retryable: true,
          cause: error
        }),
        {
          source: this.id,
          operation: context.operation || 'websocket',
          ...context
        }
      );
    }
  }
  
  handleClose(event) {
    console.log(`WebSocket connection closed (${this.url}): ${event.code} ${event.reason}`);
    this.connected = false;
    
    // Attempt to reconnect if the closure was unexpected
    if (event.code !== 1000 && event.code !== 1001) {
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        const delay = this.reconnectDelay * 
          Math.pow(this.reconnectBackoffFactor, this.reconnectAttempts);
          
        console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`);
        
        setTimeout(() => {
          this.reconnectAttempts++;
          this.attemptConnect().catch(error => {
            console.error(`Reconnection attempt failed:`, error);
          });
        }, delay);
      } else {
        console.warn(`Max reconnection attempts exceeded (${this.maxReconnectAttempts})`);
        
        // Notify any waiting promises that we're done
        if (this.resolveNextMessage) {
          this.resolveNextMessage(null);
          this.resolveNextMessage = null;
        }
      }
    }
  }
  
  // Send a message to the WebSocket
  send(message) {
    if (!this.connected || !this.socket) {
      throw new Error(`Cannot send message: WebSocket is not connected`);
    }
    
    const data = typeof message === 'string' ? message : JSON.stringify(message);
    this.socket.send(data);
  }
  
  // Async generator for WebSocket messages
  async *stream() {
    try {
      // Ensure we're connected
      if (!this.connected) {
        await this.connect();
      }
      
      while (this.connected) {
        let message;
        
        // Get message from queue or wait for new one
        if (this.messageQueue.length > 0) {
          message = this.messageQueue.shift();
        } else {
          message = await new Promise((resolve) => {
            this.resolveNextMessage = resolve;
          });
        }
        
        // If message is null, the connection was closed
        if (message === null) {
          break;
        }
        
        yield message;
      }
    } finally {
      console.log(`Stream ended for ${this.id}`);
    }
  }
}

// Server-Sent Events source
export class EventSourceWrapper extends DataSource {
  constructor(id, url, options = {}) {
    super(id, options);
    this.url = url;
    this.eventSource = null;
    this.messageQueue = [];
    this.resolveNextMessage = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
  }
  
  async connect() {
    if (this.connected) return;
    
    try {
      // Create EventSource
      this.eventSource = new EventSource(this.url);
      
      // Set up event handlers
      this.eventSource.addEventListener('open', this.handleOpen.bind(this));
      this.eventSource.addEventListener('error', this.handleError.bind(this));
      this.eventSource.addEventListener('message', this.handleMessage.bind(this));
      
      // Add custom event listeners if specified
      if (this.options.events) {
        for (const [event, handler] of Object.entries(this.options.events)) {
          this.eventSource.addEventListener(event, handler);
        }
      }
      
      // Wait for connection
      await new Promise((resolve, reject) => {
        const onOpen = () => {
          this.eventSource.removeEventListener('open', onOpen);
          this.eventSource.removeEventListener('error', onError);
          resolve();
        };
        
        const onError = (error) => {
          this.eventSource.removeEventListener('open', onOpen);
          this.eventSource.removeEventListener('error', onError);
          reject(new NetworkError(`Failed to connect to EventSource ${this.url}`, {
            retryable: true,
            cause: error
          }));
        };
        
        this.eventSource.addEventListener('open', onOpen);
        this.eventSource.addEventListener('error', onError);
        
        // Also set a timeout in case neither event fires
        setTimeout(() => {
          if (this.eventSource.readyState !== 1) { // 1 = OPEN
            reject(new TimeoutError(`Connection to EventSource ${this.url} timed out`, {
              endpoint: this.url,
              timeoutMs: 10000
            }));
          }
        }, 10000);
      });
      
      this.connected = true;
      this.reconnectAttempts = 0;
      console.log(`Connected to EventSource: ${this.url}`);
    } catch (error) {
      console.error(`EventSource connection error (${this.url}):`, error);
      
      // Clean up failed connection
      if (this.eventSource) {
        this.eventSource.close();
        this.eventSource = null;
      }
      
      // Try to reconnect
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        console.log(`Reconnecting EventSource (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})...`);
        this.reconnectAttempts++;
        
        // Wait before reconnecting
        await new Promise(resolve => setTimeout(resolve, 2000));
        return this.connect();
      }
      
      // Handle error
      await this.handleError(error, { operation: 'connect' });
      throw error;
    }
  }
  
  async disconnect() {
    if (!this.connected || !this.eventSource) return;
    
    this.eventSource.close();
    this.eventSource = null;
    this.connected = false;
    this.messageQueue = [];
    
    if (this.resolveNextMessage) {
      this.resolveNextMessage(null);
      this.resolveNextMessage = null;
    }
    
    console.log(`Disconnected from EventSource: ${this.url}`);
  }
  
  handleOpen(event) {
    console.log(`EventSource connection established (${this.url})`);
  }
  
  handleMessage(event) {
    try {
      // Parse message data
      const data = JSON.parse(event.data);
      
      // If we have a waiting resolver, resolve with this message
      if (this.resolveNextMessage) {
        const resolve = this.resolveNextMessage;
        this.resolveNextMessage = null;
        resolve({ timestamp: Date.now(), data, event: event.type });
      } else {
        // Otherwise, queue the message
        this.messageQueue.push({ 
          timestamp: Date.now(), 
          data, 
          event: event.type 
        });
      }
    } catch (error) {
      console.error(`Error parsing EventSource message (${this.url}):`, error);
    }
  }
  
  async handleError(error, context = {}) {
    console.error(`EventSource error (${this.url}):`, error);
    
    // EventSource will automatically try to reconnect,
    // but we track reconnection attempts to eventually give up
    this.reconnectAttempts++;
    
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.warn(`Max reconnection attempts exceeded (${this.maxReconnectAttempts})`);
      
      // Close the connection
      this.disconnect();
      
      // Handle error through error handler if available
      if (this.errorHandler) {
        return this.errorHandler.handleError(
          new NetworkError(`EventSource connection failed after ${this.maxReconnectAttempts} attempts`, {
            retryable: false,
            cause: error
          }),
          {
            source: this.id,
            operation: context.operation || 'eventsource',
            ...context
          }
        );
      }
    }
  }
  
  // Async generator for EventSource messages
  async *stream() {
    try {
      // Ensure we're connected
      if (!this.connected) {
        await this.connect();
      }
      
      while (this.connected) {
        let message;
        
        // Get message from queue or wait for new one
        if (this.messageQueue.length > 0) {
          message = this.messageQueue.shift();
        } else {
          message = await new Promise((resolve) => {
            this.resolveNextMessage = resolve;
          });
        }
        
        // If message is null, the connection was closed
        if (message === null) {
          break;
        }
        
        yield message;
      }
    } finally {
      console.log(`Stream ended for ${this.id}`);
    }
  }
}

// Factory function to create the appropriate data source
export function createDataSource(config) {
  const { id, type, url, options = {} } = config;
  
  switch (type.toLowerCase()) {
    case 'rest':
    case 'api':
      return new RestApiSource(id, url, options);
      
    case 'websocket':
    case 'ws':
      return new WebSocketSource(id, url, options);
      
    case 'eventsource':
    case 'sse':
      return new EventSourceWrapper(id, url, options);
      
    default:
      throw new Error(`Unknown data source type: ${type}`);
  }
}
          

Data Processing Pipeline

Now, let's implement a data processing pipeline using async generators:

Data Processing Pipeline


// data/processor.js - Data processing pipeline

import { DataError } from '../errors/index.js';

export class DataProcessor {
  constructor() {
    this.transformations = new Map();
    this.filters = new Map();
    
    // Register some common transformations
    this.registerTransformation('map', this.mapTransformation);
    this.registerTransformation('filter', this.filterTransformation);
    this.registerTransformation('reduce', this.reduceTransformation);
    this.registerTransformation('aggregate', this.aggregateTransformation);
    this.registerTransformation('sort', this.sortTransformation);
    this.registerTransformation('limit', this.limitTransformation);
  }
  
  // Register a new transformation
  registerTransformation(name, transformFn) {
    this.transformations.set(name, transformFn);
  }
  
  // Register a new filter
  registerFilter(name, filterFn) {
    this.filters.set(name, filterFn);
  }
  
  // Process a data stream through a pipeline of transformations
  async *process(source, pipeline) {
    try {
      for await (const item of source) {
        try {
          // Apply each transformation in the pipeline
          let result = item;
          
          for (const step of pipeline) {
            const { type, options } = step;
            
            // Get the transformation function
            const transformFn = this.transformations.get(type);
            
            if (!transformFn) {
              throw new DataError(`Unknown transformation: ${type}`, {
                transformation: type,
                options
              });
            }
            
            // Apply the transformation
            result = await transformFn(result, options);
            
            // If the result is null or undefined, skip further processing
            if (result === null || result === undefined) {
              break;
            }
          }
          
          // If we have a result after all transformations, yield it
          if (result !== null && result !== undefined) {
            yield result;
          }
        } catch (error) {
          console.error('Error processing data item:', error);
          
          // Yield an error result but continue processing
          yield { error, sourceItem: item };
        }
      }
    } catch (error) {
      console.error('Error in data source:', error);
      throw error;
    }
  }
  
  // Standard transformations
  
  // Map transformation
  async mapTransformation(item, options) {
    const { fn, fields } = options;
    
    if (fn && typeof fn === 'function') {
      // Apply custom function
      return fn(item);
    } else if (fields) {
      // Pick specified fields
      const result = {};
      
      for (const field of fields) {
        if (typeof field === 'string') {
          result[field] = item.data ? item.data[field] : item[field];
        } else if (typeof field === 'object') {
          // { source: 'originalName', target: 'newName' }
          const { source, target } = field;
          result[target] = item.data ? item.data[source] : item[source];
        }
      }
      
      return {
        ...item,
        data: result
      };
    }
    
    return item;
  }
  
  // Filter transformation
  async filterTransformation(item, options) {
    const { fn, condition } = options;
    
    if (fn && typeof fn === 'function') {
      // Apply custom function
      if (fn(item)) {
        return item;
      }
      return null;
    } else if (condition) {
      // { field: 'price', operator: 'gt', value: 100 }
      const { field, operator, value } = condition;
      const itemValue = item.data ? item.data[field] : item[field];
      
      switch (operator) {
        case 'eq':
          if (itemValue === value) return item;
          break;
        case 'neq':
          if (itemValue !== value) return item;
          break;
        case 'gt':
          if (itemValue > value) return item;
          break;
        case 'gte':
          if (itemValue >= value) return item;
          break;
        case 'lt':
          if (itemValue < value) return item;
          break;
        case 'lte':
          if (itemValue <= value) return item;
          break;
        case 'contains':
          if (typeof itemValue === 'string' && itemValue.includes(value)) return item;
          break;
        case 'in':
          if (Array.isArray(value) && value.includes(itemValue)) return item;
          break;
        default:
          console.warn(`Unknown operator: ${operator}`);
          return item;
      }
      
      return null;
    }
    
    return item;
  }
  
  // Reduce transformation (for windows of data)
  async reduceTransformation(item, options) {
    const { fn, initialValue } = options;
    
    if (!Array.isArray(item.data)) {
      return item;
    }
    
    const result = item.data.reduce(fn, initialValue);
    
    return {
      ...item,
      data: result
    };
  }
  
  // Aggregate transformation (for financial calculations)
  async aggregateTransformation(item, options) {
    const { type, field } = options;
    
    if (!item.data || !Array.isArray(item.data)) {
      return item;
    }
    
    let result;
    
    switch (type) {
      case 'sum':
        result = item.data.reduce((sum, current) => {
          return sum + (current[field] || 0);
        }, 0);
        break;
        
      case 'avg':
        if (item.data.length === 0) {
          result = 0;
        } else {
          const sum = item.data.reduce((acc, current) => {
            return acc + (current[field] || 0);
          }, 0);
          result = sum / item.data.length;
        }
        break;
        
      case 'min':
        result = Math.min(...item.data.map(d => d[field] || 0));
        break;
        
      case 'max':
        result = Math.max(...item.data.map(d => d[field] || 0));
        break;
        
      case 'count':
        result = item.data.length;
        break;
        
      case 'sma': // Simple Moving Average
        const period = options.period || 10;
        if (item.data.length < period) {
          result = null;
        } else {
          const values = item.data.slice(-period).map(d => d[field] || 0);
          result = values.reduce((a, b) => a + b, 0) / period;
        }
        break;
        
      default:
        result = item.data;
        break;
    }
    
    return {
      ...item,
      data: result
    };
  }
  
  // Sort transformation
  async sortTransformation(item, options) {
    const { field, direction = 'asc' } = options;
    
    if (!item.data || !Array.isArray(item.data)) {
      return item;
    }
    
    const sortedData = [...item.data].sort((a, b) => {
      if (a[field] < b[field]) return direction === 'asc' ? -1 : 1;
      if (a[field] > b[field]) return direction === 'asc' ? 1 : -1;
      return 0;
    });
    
    return {
      ...item,
      data: sortedData
    };
  }
  
  // Limit transformation
  async limitTransformation(item, options) {
    const { count } = options;
    
    if (!item.data || !Array.isArray(item.data)) {
      return item;
    }
    
    return {
      ...item,
      data: item.data.slice(0, count)
    };
  }
  
  // Create a pipeline from a configuration object
  createPipeline(config) {
    if (!Array.isArray(config)) {
      throw new Error('Pipeline configuration must be an array');
    }
    
    return config.map(step => {
      const { type, options = {} } = step;
      
      if (!type) {
        throw new Error('Pipeline step must have a type');
      }
      
      return { type, options };
    });
  }
}

// Pipeline operators for financial calculations
export const financialOperators = {
  // Calculate Simple Moving Average
  sma(data, field, period) {
    if (data.length < period) {
      return null;
    }
    
    const values = data.slice(-period).map(d => d[field]);
    return values.reduce((a, b) => a + b, 0) / period;
  },
  
  // Calculate Exponential Moving Average
  ema(data, field, period, smoothing = 2) {
    if (data.length < period) {
      return null;
    }
    
    const values = data.map(d => d[field]);
    const k = smoothing / (period + 1);
    
    // First EMA is SMA
    let ema = values.slice(0, period).reduce((a, b) => a + b, 0) / period;
    
    // Calculate EMA for the rest
    for (let i = period; i < values.length; i++) {
      ema = values[i] * k + ema * (1 - k);
    }
    
    return ema;
  },
  
  // Calculate Relative Strength Index
  rsi(data, field, period = 14) {
    if (data.length <= period) {
      return null;
    }
    
    const values = data.map(d => d[field]);
    let gains = 0;
    let losses = 0;
    
    // Calculate initial average gain/loss
    for (let i = 1; i <= period; i++) {
      const change = values[i] - values[i - 1];
      if (change >= 0) {
        gains += change;
      } else {
        losses -= change;
      }
    }
    
    let avgGain = gains / period;
    let avgLoss = losses / period;
    
    // Calculate RSI for the rest of the data
    for (let i = period + 1; i < values.length; i++) {
      const change = values[i] - values[i - 1];
      
      if (change >= 0) {
        avgGain = (avgGain * (period - 1) + change) / period;
        avgLoss = (avgLoss * (period - 1)) / period;
      } else {
        avgGain = (avgGain * (period - 1)) / period;
        avgLoss = (avgLoss * (period - 1) - change) / period;
      }
    }
    
    if (avgLoss === 0) {
      return 100;
    }
    
    const rs = avgGain / avgLoss;
    return 100 - (100 / (1 + rs));
  },
  
  // Calculate MACD (Moving Average Convergence Divergence)
  macd(data, field, shortPeriod = 12, longPeriod = 26, signalPeriod = 9) {
    if (data.length < Math.max(shortPeriod, longPeriod) + signalPeriod) {
      return null;
    }
    
    const values = data.map(d => d[field]);
    
    // Calculate EMAs
    const shortEMA = this.ema(data, field, shortPeriod);
    const longEMA = this.ema(data, field, longPeriod);
    
    // MACD Line = 12-day EMA - 26-day EMA
    const macdLine = shortEMA - longEMA;
    
    // TODO: Calculate signal line (9-day EMA of MACD Line)
    // For a real implementation, you'd need to track MACD history
    
    return {
      macd: macdLine,
      signal: null, // Not calculated in this simplified version
      histogram: null // Not calculated in this simplified version
    };
  }
};
          

Visualization Component

Finally, let's implement a visualization component to display our processed data:

Chart Visualization Component


// visualization/chart.js - Chart visualization

export class ChartManager {
  constructor() {
    this.charts = new Map();
    this.colors = [
      '#4285F4', '#EA4335', '#FBBC05', '#34A853', // Google colors
      '#3498db', '#e74c3c', '#2ecc71', '#f39c12', // Flat UI colors
      '#9b59b6', '#1abc9c', '#e67e22', '#95a5a6'  // More Flat UI colors
    ];
  }
  
  // Create a new chart
  createChart(id, container, config) {
    if (this.charts.has(id)) {
      console.warn(`Chart with ID ${id} already exists and will be replaced`);
      this.destroyChart(id);
    }
    
    const { type = 'line', options = {} } = config;
    
    // Create canvas element
    const canvas = document.createElement('canvas');
    container.appendChild(canvas);
    
    // Create default options based on chart type
    const defaultOptions = this.getDefaultOptions(type);
    
    // Merge with user options
    const chartOptions = {
      ...defaultOptions,
      ...options
    };
    
    // Create the chart
    const chart = new Chart(canvas.getContext('2d'), {
      type,
      data: {
        labels: [],
        datasets: []
      },
      options: chartOptions
    });
    
    // Store the chart with metadata
    this.charts.set(id, {
      instance: chart,
      config,
      container,
      canvas,
      datasets: new Map()
    });
    
    return chart;
  }
  
  // Get default options for chart types
  getDefaultOptions(type) {
    const baseOptions = {
      responsive: true,
      maintainAspectRatio: false,
      animation: {
        duration: 500
      },
      plugins: {
        legend: {
          position: 'top',
        },
        tooltip: {
          mode: 'index',
          intersect: false
        }
      }
    };
    
    switch (type) {
      case 'line':
        return {
          ...baseOptions,
          scales: {
            x: {
              type: 'time',
              time: {
                unit: 'minute',
                tooltipFormat: 'HH:mm:ss',
                displayFormats: {
                  minute: 'HH:mm'
                }
              },
              title: {
                display: true,
                text: 'Time'
              }
            },
            y: {
              beginAtZero: false,
              title: {
                display: true,
                text: 'Value'
              }
            }
          },
          elements: {
            line: {
              tension: 0.3
            },
            point: {
              radius: 0,
              hitRadius: 10,
              hoverRadius: 4
            }
          }
        };
        
      case 'bar':
        return {
          ...baseOptions,
          scales: {
            x: {
              grid: {
                offset: true
              }
            },
            y: {
              beginAtZero: true
            }
          }
        };
        
      case 'pie':
      case 'doughnut':
        return {
          ...baseOptions,
          plugins: {
            ...baseOptions.plugins,
            legend: {
              position: 'right'
            }
          }
        };
        
      case 'radar':
        return {
          ...baseOptions,
          elements: {
            line: {
              tension: 0.3
            }
          }
        };
        
      default:
        return baseOptions;
    }
  }
  
  // Add or update a dataset in a chart
  updateDataset(chartId, datasetId, data) {
    const chartInfo = this.charts.get(chartId);
    
    if (!chartInfo) {
      console.error(`Chart with ID ${chartId} not found`);
      return;
    }
    
    const { instance: chart, datasets } = chartInfo;
    
    // Check if the dataset already exists
    if (!datasets.has(datasetId)) {
      // Create a new dataset
      const datasetIndex = chart.data.datasets.length;
      const colorIndex = datasetIndex % this.colors.length;
      
      const newDataset = {
        label: datasetId,
        data: [],
        borderColor: this.colors[colorIndex],
        backgroundColor: this.colors[colorIndex] + '80', // Add transparency
        borderWidth: 2,
        fill: false
      };
      
      // Add dataset to the chart
      chart.data.datasets.push(newDataset);
      
      // Store dataset info
      datasets.set(datasetId, {
        index: datasetIndex,
        config: newDataset
      });
    }
    
    // Get dataset info
    const datasetInfo = datasets.get(datasetId);
    const dataset = chart.data.datasets[datasetInfo.index];
    
    // Update the data
    if (Array.isArray(data)) {
      // Replace all data
      dataset.data = data;
    } else {
      // Append a single data point
      dataset.data.push(data);
      
      // If we have too many points, remove the oldest
      const maxPoints = 100; // Configurable
      if (dataset.data.length > maxPoints) {
        dataset.data.shift();
      }
    }
    
    // Update labels if needed (for time series)
    if (data.x && chart.data.labels.length < dataset.data.length) {
      chart.data.labels.push(data.x);
      
      // Remove excess labels
      while (chart.data.labels.length > dataset.data.length) {
        chart.data.labels.shift();
      }
    }
    
    // Update the chart
    chart.update();
  }
  
  // Destroy a chart
  destroyChart(id) {
    const chartInfo = this.charts.get(id);
    
    if (!chartInfo) {
      console.warn(`Chart with ID ${id} not found`);
      return;
    }
    
    const { instance, container, canvas } = chartInfo;
    
    // Destroy Chart.js instance
    instance.destroy();
    
    // Remove canvas from container
    if (container.contains(canvas)) {
      container.removeChild(canvas);
    }
    
    // Remove from charts map
    this.charts.delete(id);
  }
  
  // Get a chart instance
  getChart(id) {
    const chartInfo = this.charts.get(id);
    return chartInfo ? chartInfo.instance : null;
  }
  
  // Clear all data from a chart
  clearChart(id) {
    const chartInfo = this.charts.get(id);
    
    if (!chartInfo) {
      console.warn(`Chart with ID ${id} not found`);
      return;
    }
    
    const { instance: chart } = chartInfo;
    
    // Clear all data
    chart.data.labels = [];
    chart.data.datasets.forEach(dataset => {
      dataset.data = [];
    });
    
    // Update the chart
    chart.update();
  }
  
  // Create a real-time streaming chart
  createStreamingChart(id, container, config) {
    const chart = this.createChart(id, container, {
      ...config,
      options: {
        ...config.options,
        scales: {
          x: {
            type: 'realtime',
            realtime: {
              duration: 60000, // 1 minute
              refresh: 1000,   // 1 second
              delay: 1000,     // 1 second
              onRefresh: chart => {
                // This will be handled by our update method
              }
            }
          },
          y: {
            beginAtZero: false,
            title: {
              display: true,
              text: config.options?.scales?.y?.title?.text || 'Value'
            }
          }
        },
        plugins: {
          ...config.options?.plugins,
          streaming: {
            frameRate: 30
          }
        }
      }
    });
    
    return chart;
  }
  
  // Update a streaming chart with new data
  updateStreamingChart(chartId, datasetId, value, timestamp = Date.now()) {
    const chartInfo = this.charts.get(chartId);
    
    if (!chartInfo) {
      console.error(`Chart with ID ${chartId} not found`);
      return;
    }
    
    this.updateDataset(chartId, datasetId, {
      x: timestamp,
      y: value
    });
  }
}
          

Step 4: Look Back and Reflect

Now that we've implemented the core components of our data-driven application, it's important to step back, reflect on our solution, and refine it.

Integration: Bringing Everything Together

Let's implement a central Dashboard Controller that integrates all our components:

Dashboard Controller


// DashboardController.js - Main application controller

import { ErrorHandler } from './errors/index.js';
import { createDataSource } from './data/sources.js';
import { DataProcessor, financialOperators } from './data/processor.js';
import { ChartManager } from './visualization/chart.js';
import { retry, withTimeout, limitConcurrency } from './utils/async.js';

export class DashboardController {
  constructor(config = {}) {
    // Initialize subsystems
    this.errorHandler = new ErrorHandler();
    this.processor = new DataProcessor();
    this.chartManager = new ChartManager();
    
    // Initialize state
    this.sources = new Map();
    this.widgets = new Map();
    this.running = false;
    this.abortController = new AbortController();
    
    // Register financial operators
    Object.entries(financialOperators).forEach(([name, fn]) => {
      this.processor.registerTransformation(name, fn);
    });
    
    // Apply initial configuration
    if (config.sources) {
      this.configureSources(config.sources);
    }
    
    if (config.widgets) {
      this.configureWidgets(config.widgets);
    }
    
    // Store user preferences
    this.preferences = this.loadPreferences();
  }
  
  // Initialize the dashboard
  async init() {
    try {
      // Create DOM elements for widgets
      this.createWidgetContainers();
      
      // Initialize data sources
      await this.initializeSources();
      
      // Initialize widgets
      this.initializeWidgets();
      
      console.log('Dashboard initialized successfully');
      return true;
    } catch (error) {
      console.error('Failed to initialize dashboard:', error);
      
      // Handle error
      const errorResult = await this.errorHandler.handleError(error, {
        operation: 'init'
      });
      
      // Display error message
      this.showErrorMessage('Failed to initialize dashboard', errorResult);
      
      return false;
    }
  }
  
  // Start the dashboard
  async start() {
    if (this.running) {
      console.warn('Dashboard is already running');
      return;
    }
    
    try {
      this.running = true;
      this.abortController = new AbortController();
      
      // Connect to all data sources
      const connectPromises = Array.from(this.sources.values()).map(source => 
        source.connect().catch(error => {
          console.error(`Failed to connect to source ${source.id}:`, error);
          return { error, source: source.id };
        })
      );
      
      // Wait for all connections
      const results = await Promise.allSettled(connectPromises);
      
      // Check for connection failures
      const failures = results
        .filter(result => result.status === 'rejected' || result.value?.error)
        .map(result => {
          if (result.status === 'rejected') {
            return { error: result.reason, source: 'unknown' };
          }
          return result.value;
        });
      
      if (failures.length > 0) {
        console.warn(`${failures.length} data sources failed to connect:`, failures);
        
        // Show warning to user
        this.showWarningMessage(`${failures.length} data sources failed to connect. Some widgets may not display correctly.`);
      }
      
      // Start data streams for each widget
      for (const [id, widget] of this.widgets.entries()) {
        this.startWidgetStream(id, widget);
      }
      
      console.log('Dashboard started');
    } catch (error) {
      console.error('Failed to start dashboard:', error);
      this.running = false;
      
      // Handle error
      const errorResult = await this.errorHandler.handleError(error, {
        operation: 'start'
      });
      
      // Display error message
      this.showErrorMessage('Failed to start dashboard', errorResult);
    }
  }
  
  // Stop the dashboard
  async stop() {
    if (!this.running) {
      console.warn('Dashboard is not running');
      return;
    }
    
    try {
      // Signal abort to all streams
      this.abortController.abort();
      
      // Disconnect from all data sources
      const disconnectPromises = Array.from(this.sources.values()).map(source => 
        source.disconnect().catch(error => {
          console.error(`Failed to disconnect from source ${source.id}:`, error);
        })
      );
      
      // Wait for all disconnections
      await Promise.allSettled(disconnectPromises);
      
      this.running = false;
      console.log('Dashboard stopped');
    } catch (error) {
      console.error('Error stopping dashboard:', error);
      
      // Force stop
      this.running = false;
    }
  }
  
  // Configure data sources
  configureSources(sourcesConfig) {
    for (const sourceConfig of sourcesConfig) {
      const { id } = sourceConfig;
      
      try {
        // Create the data source
        const source = createDataSource({
          ...sourceConfig,
          options: {
            ...sourceConfig.options,
            errorHandler: this.errorHandler
          }
        });
        
        // Add to sources map
        this.sources.set(id, source);
      } catch (error) {
        console.error(`Failed to configure source ${id}:`, error);
      }
    }
  }
  
  // Configure widgets
  configureWidgets(widgetsConfig) {
    for (const widgetConfig of widgetsConfig) {
      const { id } = widgetConfig;
      
      // Add to widgets map
      this.widgets.set(id, widgetConfig);
    }
  }
  
  // Initialize data sources
  async initializeSources() {
    const initPromises = Array.from(this.sources.values()).map(source => {
      // Just create the source, don't connect yet
      return Promise.resolve();
    });
    
    // Wait for all initializations
    await Promise.all(initPromises);
  }
  
  // Create widget containers
  createWidgetContainers() {
    const dashboard = document.getElementById('dashboard');
    
    if (!dashboard) {
      console.error('Dashboard container not found');
      return;
    }
    
    // Clear existing widgets
    dashboard.innerHTML = '';
    
    // Create containers for each widget
    for (const [id, widget] of this.widgets.entries()) {
      const { title, type, width = 6, height = 1 } = widget;
      
      // Create widget container
      const container = document.createElement('div');
      container.className = `widget widget-${type}`;
      container.style.gridColumn = `span ${width}`;
      container.style.gridRow = `span ${height}`;
      container.id = `widget-${id}`;
      
      // Create widget header
      const header = document.createElement('div');
      header.className = 'widget-header';
      
      const titleElem = document.createElement('h3');
      titleElem.className = 'widget-title';
      titleElem.textContent = title;
      
      const actions = document.createElement('div');
      actions.className = 'widget-actions';
      
      // Add refresh button
      const refreshBtn = document.createElement('button');
      refreshBtn.className = 'widget-refresh';
      refreshBtn.innerHTML = '↻'; // Refresh icon
      refreshBtn.title = 'Refresh';
      refreshBtn.addEventListener('click', () => this.refreshWidget(id));
      
      // Add settings button
      const settingsBtn = document.createElement('button');
      settingsBtn.className = 'widget-settings';
      settingsBtn.innerHTML = '⚙'; // Settings icon
      settingsBtn.title = 'Settings';
      settingsBtn.addEventListener('click', () => this.openWidgetSettings(id));
      
      // Add buttons to actions
      actions.appendChild(refreshBtn);
      actions.appendChild(settingsBtn);
      
      // Add elements to header
      header.appendChild(titleElem);
      header.appendChild(actions);
      
      // Create widget content
      const content = document.createElement('div');
      content.className = 'widget-content';
      content.id = `widget-content-${id}`;
      
      // Create widget footer for status
      const footer = document.createElement('div');
      footer.className = 'widget-footer';
      footer.id = `widget-footer-${id}`;
      
      // Add status indicator
      const status = document.createElement('span');
      status.className = 'widget-status';
      status.id = `widget-status-${id}`;
      status.textContent = 'Initializing...';
      
      footer.appendChild(status);
      
      // Assemble widget
      container.appendChild(header);
      container.appendChild(content);
      container.appendChild(footer);
      
      // Add to dashboard
      dashboard.appendChild(container);
    }
  }
  
  // Initialize widgets
  initializeWidgets() {
    for (const [id, widget] of this.widgets.entries()) {
      const { type, chartType, dataSource } = widget;
      
      try {
        // Get the content container
        const container = document.getElementById(`widget-content-${id}`);
        
        if (!container) {
          console.error(`Container for widget ${id} not found`);
          continue;
        }
        
        // Initialize based on widget type
        switch (type) {
          case 'chart':
            // Create chart
            this.chartManager.createChart(id, container, {
              type: chartType || 'line',
              options: widget.chartOptions || {}
            });
            break;
            
          case 'table':
            // Create table
            container.innerHTML = `
              
                    ${(widget.columns || []).map(col => ``).join('')}
                  
${col.label}
`; break; case 'metrics': // Create metrics display container.innerHTML = `
${(widget.metrics || []).map(metric => `
--
${metric.label}
`).join('')}
`; break; default: console.warn(`Unknown widget type: ${type}`); break; } // Update status this.updateWidgetStatus(id, 'Ready'); } catch (error) { console.error(`Error initializing widget ${id}:`, error); this.updateWidgetStatus(id, 'Error initializing'); } } } // Start data stream for widget async startWidgetStream(id, widget) { const { dataSource: sourceId, pipeline = [] } = widget; // Get data source const source = this.sources.get(sourceId); if (!source) { console.error(`Data source ${sourceId} not found for widget ${id}`); this.updateWidgetStatus(id, 'Error: Data source not found'); return; } try { // Create processing pipeline const processingPipeline = this.processor.createPipeline(pipeline); // Create data stream const stream = this.processor.process(source.stream(), processingPipeline); // Start consuming the stream this.consumeWidgetStream(id, widget, stream); // Update status this.updateWidgetStatus(id, 'Live'); } catch (error) { console.error(`Error starting stream for widget ${id}:`, error); this.updateWidgetStatus(id, 'Error: Failed to start data stream'); } } // Consume data stream for widget async consumeWidgetStream(id, widget, stream) { const { type } = widget; const signal = this.abortController.signal; try { // Process the stream based on widget type for await (const item of stream) { // Check if we've been aborted if (signal.aborted) { break; } // Handle error items if (item.error) { console.error(`Error in data stream for widget ${id}:`, item.error); this.updateWidgetStatus(id, 'Error in data stream'); continue; } // Update the widget with new data this.updateWidget(id, widget, item); } } catch (error) { console.error(`Error consuming stream for widget ${id}:`, error); this.updateWidgetStatus(id, 'Error: Stream processing failed'); } } // Update widget with new data updateWidget(id, widget, data) { const { type, dataField } = widget; try { switch (type) { case 'chart': this.updateChartWidget(id, widget, data); break; case 'table': this.updateTableWidget(id, widget, data); break; case 'metrics': this.updateMetricsWidget(id, widget, data); break; default: console.warn(`Unknown widget type: ${type}`); break; } // Update last updated timestamp this.updateWidgetStatus(id, 'Updated ' + new Date().toLocaleTimeString()); } catch (error) { console.error(`Error updating widget ${id}:`, error); this.updateWidgetStatus(id, 'Error updating widget'); } } // Update chart widget updateChartWidget(id, widget, data) { const { datasets = [] } = widget; // Get the data to display const itemData = data.data; // Update each dataset datasets.forEach(dataset => { const { id: datasetId, field, transform } = dataset; let value = itemData[field]; // Apply transformation if specified if (transform && typeof transform === 'function') { value = transform(value, itemData); } // Update the chart this.chartManager.updateDataset(id, datasetId, { x: data.timestamp, y: value }); }); } // Update table widget updateTableWidget(id, widget, data) { const { columns = [], maxRows = 10 } = widget; // Get table body const tableBody = document.querySelector(`#table-${id} tbody`); if (!tableBody) { console.error(`Table body for widget ${id} not found`); return; } // Get the data to display const itemData = data.data; // Create a new row const row = document.createElement('tr'); // Add cells for each column columns.forEach(column => { const { field, format } = column; const cell = document.createElement('td'); // Get the value let value = itemData[field]; // Format the value if specified if (format && typeof format === 'function') { value = format(value, itemData); } cell.textContent = value; row.appendChild(cell); }); // Add the row to the table tableBody.appendChild(row); // Remove excess rows while (tableBody.children.length > maxRows) { tableBody.removeChild(tableBody.firstChild); } } // Update metrics widget updateMetricsWidget(id, widget, data) { const { metrics = [] } = widget; // Get the data to display const itemData = data.data; // Update each metric metrics.forEach(metric => { const { id: metricId, field, format } = metric; // Get the element const element = document.getElementById(`metric-${id}-${metricId}`); if (!element) { console.warn(`Metric element ${metricId} not found for widget ${id}`); return; } // Get the value let value = itemData[field]; // Format the value if specified if (format && typeof format === 'function') { value = format(value, itemData); } else if (typeof value === 'number') { // Default number formatting value = value.toLocaleString(undefined, { maximumFractionDigits: 2 }); } element.textContent = value; // Add trend indicator if available if (itemData.trend || itemData.change) { const trend = itemData.trend || (itemData.change > 0 ? 'up' : itemData.change < 0 ? 'down' : 'flat'); element.dataset.trend = trend; } }); } // Update widget status updateWidgetStatus(id, status) { const statusElement = document.getElementById(`widget-status-${id}`); if (statusElement) { statusElement.textContent = status; } } // Refresh a widget refreshWidget(id) { const widget = this.widgets.get(id); if (!widget) { console.error(`Widget ${id} not found`); return; } // Clear existing data switch (widget.type) { case 'chart': this.chartManager.clearChart(id); break; case 'table': const tableBody = document.querySelector(`#table-${id} tbody`); if (tableBody) { tableBody.innerHTML = ''; } break; case 'metrics': (widget.metrics || []).forEach(metric => { const element = document.getElementById(`metric-${id}-${metric.id}`); if (element) { element.textContent = '--'; delete element.dataset.trend; } }); break; } // Update status this.updateWidgetStatus(id, 'Refreshing...'); // If the dashboard is running, restart the stream if (this.running) { // Stop current stream // (The abort controller will handle this) // Start new stream this.startWidgetStream(id, widget); } else { this.updateWidgetStatus(id, 'Ready'); } } // Open widget settings openWidgetSettings(id) { const widget = this.widgets.get(id); if (!widget) { console.error(`Widget ${id} not found`); return; } // Create settings modal const modal = document.createElement('div'); modal.className = 'modal'; modal.innerHTML = ` <div class="modal-content"> <div class="modal-header"> <h2>Widget Settings: ${widget.title}</h2> <button class="modal-close">×</button> </div> <div class="modal-body"> <form id="widget-settings-form"> <div class="form-group"> <label for="widget-title">Title</label> <input type="text" id="widget-title" value="${widget.title}"> </div> <!-- Add more settings based on widget type --> ${this.getWidgetSettingsFields(widget)} <div class="form-actions"> <button type="submit" class="btn btn-primary">Save</button> <button type="button" class="btn btn-cancel">Cancel</button> </div> </form> </div> </div> `; // Add to document document.body.appendChild(modal); // Add event listeners modal.querySelector('.modal-close').addEventListener('click', () => { document.body.removeChild(modal); }); modal.querySelector('.btn-cancel').addEventListener('click', () => { document.body.removeChild(modal); }); modal.querySelector('#widget-settings-form').addEventListener('submit', (event) => { event.preventDefault(); // Get updated settings const title = modal.querySelector('#widget-title').value; // Update widget widget.title = title; // Update widget-specific settings this.updateWidgetFromSettings(widget, modal); // Update widget display document.querySelector(`#widget-${id} .widget-title`).textContent = title; // Refresh widget this.refreshWidget(id); // Save preferences this.savePreferences(); // Close modal document.body.removeChild(modal); }); } // Get widget-specific settings fields getWidgetSettingsFields(widget) { const { type } = widget; switch (type) { case 'chart': return ` <div class="form-group"> <label for="widget-chart-type">Chart Type</label> <select id="widget-chart-type"> <option value="line" ${widget.chartType === 'line' ? 'selected' : ''}>Line</option> <option value="bar" ${widget.chartType === 'bar' ? 'selected' : ''}>Bar</option> <option value="pie" ${widget.chartType === 'pie' ? 'selected' : ''}>Pie</option> </select> </div> `; case 'table': return ` <div class="form-group"> <label for="widget-max-rows">Maximum Rows</label> <input type="number" id="widget-max-rows" value="${widget.maxRows || 10}" min="1" max="100"> </div> `; default: return ''; } } // Update widget settings from form updateWidgetFromSettings(widget, modal) { const { type } = widget; switch (type) { case 'chart': widget.chartType = modal.querySelector('#widget-chart-type').value; break; case 'table': widget.maxRows = parseInt(modal.querySelector('#widget-max-rows').value, 10); break; } } // Load user preferences loadPreferences() { try { const savedPreferences = localStorage.getItem('dashboardPreferences'); if (savedPreferences) { return JSON.parse(savedPreferences); } } catch (error) { console.error('Error loading preferences:', error); } return {}; } // Save user preferences savePreferences() { try { const preferences = { widgets: Object.fromEntries( Array.from(this.widgets.entries()).map(([id, widget]) => [ id, { title: widget.title, chartType: widget.chartType, maxRows: widget.maxRows } ]) ) }; localStorage.setItem('dashboardPreferences', JSON.stringify(preferences)); } catch (error) { console.error('Error saving preferences:', error); } } // Show error message showErrorMessage(message, details) { // Create notification const notification = document.createElement('div'); notification.className = 'notification error'; notification.innerHTML = ` <div class="notification-content"> <div class="notification-title">Error</div> <div class="notification-message">${message}</div> ${details ? `<div class="notification-details">${details.error || ''}</div>` : ''} <button class="notification-close">×</button> </div> `; // Add to notification container const container = document.getElementById('notifications') || document.body; container.appendChild(notification); // Add event listener for close button notification.querySelector('.notification-close').addEventListener('click', () => { container.removeChild(notification); }); // Auto-remove after 10 seconds setTimeout(() => { if (container.contains(notification)) { container.removeChild(notification); } }, 10000); } // Show warning message showWarningMessage(message) { // Create notification const notification = document.createElement('div'); notification.className = 'notification warning'; notification.innerHTML = ` <div class="notification-content"> <div class="notification-title">Warning</div> <div class="notification-message">${message}</div> <button class="notification-close">×</button> </div> `; // Add to notification container const container = document.getElementById('notifications') || document.body; container.appendChild(notification); // Add event listener for close button notification.querySelector('.notification-close').addEventListener('click', () => { container.removeChild(notification); }); // Auto-remove after 7 seconds setTimeout(() => { if (container.contains(notification)) { container.removeChild(notification); } }, 7000); } }

Improvements and Optimizations

After reviewing our implementation, we can identify several areas for improvement:

Optimizing Chart Updates


// Optimization: Batch chart updates
class OptimizedChartManager extends ChartManager {
  constructor() {
    super();
    this.updateQueue = new Map();
    this.animationFrameRequested = false;
  }
  
  // Override update method to batch updates
  updateDataset(chartId, datasetId, data) {
    // Get or create queue for this chart
    if (!this.updateQueue.has(chartId)) {
      this.updateQueue.set(chartId, new Map());
    }
    
    const chartQueue = this.updateQueue.get(chartId);
    
    // Queue this update
    if (!chartQueue.has(datasetId)) {
      chartQueue.set(datasetId, []);
    }
    
    chartQueue.get(datasetId).push(data);
    
    // Request animation frame for processing
    if (!this.animationFrameRequested) {
      this.animationFrameRequested = true;
      requestAnimationFrame(() => this.processUpdateQueue());
    }
  }
  
  // Process all queued updates
  processUpdateQueue() {
    this.animationFrameRequested = false;
    
    // Process each chart
    for (const [chartId, chartQueue] of this.updateQueue.entries()) {
      const chartInfo = this.charts.get(chartId);
      
      if (!chartInfo) {
        continue;
      }
      
      // Apply updates to datasets
      for (const [datasetId, updates] of chartQueue.entries()) {
        const datasetInfo = chartInfo.datasets.get(datasetId);
        
        if (!datasetInfo) {
          continue;
        }
        
        const dataset = chartInfo.instance.data.datasets[datasetInfo.index];
        
        // Apply each update
        for (const update of updates) {
          if (Array.isArray(update)) {
            // Replace all data
            dataset.data = update;
          } else {
            // Append a single data point
            dataset.data.push(update);
            
            // If we have too many points, remove the oldest
            const maxPoints = 100; // Configurable
            if (dataset.data.length > maxPoints) {
              dataset.data.shift();
            }
            
            // Update labels if needed (for time series)
            if (update.x && chartInfo.instance.data.labels.length < dataset.data.length) {
              chartInfo.instance.data.labels.push(update.x);
              
              // Remove excess labels
              while (chartInfo.instance.data.labels.length > dataset.data.length) {
                chartInfo.instance.data.labels.shift();
              }
            }
          }
        }
      }
      
      // Update the chart
      chartInfo.instance.update();
    }
    
    // Clear the queue
    this.updateQueue.clear();
  }
}
          

Project Setup and Configuration

To help you get started with this weekend project, here is a sample configuration for your dashboard:

Sample Dashboard Configuration


// Sample dashboard configuration
const dashboardConfig = {
  // Data sources
  sources: [
    {
      id: 'stock-api',
      type: 'rest',
      url: 'https://api.example.com/stocks',
      options: {
        interval: 60000, // 1 minute polling interval
        rateLimit: {
          maxRequests: 30,
          interval: 60000 // 30 requests per minute
        },
        headers: {
          'X-API-Key': 'your-api-key-here'
        }
      }
    },
    {
      id: 'news-feed',
      type: 'websocket',
      url: 'wss://api.example.com/news',
      options: {
        onConnect: () => ({
          action: 'subscribe',
          topics: ['finance', 'markets', 'technology']
        })
      }
    },
    {
      id: 'market-data',
      type: 'eventsource',
      url: 'https://api.example.com/market/events',
      options: {
        events: {
          'market-update': (event) => {
            console.log('Market update received:', event);
          }
        }
      }
    }
  ],
  
  // Widgets
  widgets: [
    {
      id: 'stock-chart',
      title: 'Stock Price History',
      type: 'chart',
      dataSource: 'stock-api',
      chartType: 'line',
      width: 8,
      height: 2,
      chartOptions: {
        scales: {
          y: {
            title: {
              display: true,
              text: 'Price ($)'
            }
          }
        }
      },
      datasets: [
        {
          id: 'price',
          field: 'price',
          label: 'Stock Price'
        },
        {
          id: 'sma',
          field: 'price',
          label: 'SMA (20)',
          transform: (value, data) => {
            // TODO: Calculate SMA
            return value * 0.95;
          }
        }
      ],
      pipeline: [
        {
          type: 'filter',
          options: {
            condition: {
              field: 'symbol',
              operator: 'eq',
              value: 'AAPL'
            }
          }
        }
      ]
    },
    {
      id: 'news-table',
      title: 'Latest Financial News',
      type: 'table',
      dataSource: 'news-feed',
      width: 4,
      height: 2,
      maxRows: 10,
      columns: [
        {
          field: 'time',
          label: 'Time',
          format: (value) => new Date(value).toLocaleTimeString()
        },
        {
          field: 'headline',
          label: 'Headline'
        },
        {
          field: 'source',
          label: 'Source'
        }
      ],
      pipeline: [
        {
          type: 'filter',
          options: {
            condition: {
              field: 'category',
              operator: 'in',
              value: ['stocks', 'market', 'economy']
            }
          }
        },
        {
          type: 'sort',
          options: {
            field: 'time',
            direction: 'desc'
          }
        }
      ]
    },
    {
      id: 'market-metrics',
      title: 'Market Indicators',
      type: 'metrics',
      dataSource: 'market-data',
      width: 12,
      height: 1,
      metrics: [
        {
          id: 'sp500',
          field: 'sp500',
          label: 'S&P 500',
          format: (value) => value.toFixed(2)
        },
        {
          id: 'nasdaq',
          field: 'nasdaq',
          label: 'NASDAQ',
          format: (value) => value.toFixed(2)
        },
        {
          id: 'djia',
          field: 'djia',
          label: 'Dow Jones',
          format: (value) => value.toFixed(2)
        },
        {
          id: 'volatility',
          field: 'vix',
          label: 'VIX',
          format: (value) => value.toFixed(2)
        }
      ],
      pipeline: []
    }
    // Add more widgets as needed
  ]
};

// Initialize and start the dashboard
document.addEventListener('DOMContentLoaded', async () => {
  // Create dashboard
  const dashboard = new DashboardController(dashboardConfig);
  
  // Initialize
  const initialized = await dashboard.init();
  
  if (initialized) {
    // Start
    await dashboard.start();
    
    // Add event listener for page unload
    window.addEventListener('beforeunload', () => {
      dashboard.stop();
    });
  }
});
          

HTML Structure

Here's a basic HTML structure for your dashboard:

HTML Structure


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Real-Time Financial Dashboard</title>
  <link rel="stylesheet" href="css/styles.css">
  <script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@2.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
</head>
<body>
  <header>
    <h1>Real-Time Financial Dashboard</h1>
    <div class="controls">
      <button id="start-btn" class="btn btn-primary">Start</button>
      <button id="stop-btn" class="btn btn-danger">Stop</button>
      <button id="settings-btn" class="btn btn-secondary">Settings</button>
    </div>
  </header>
  
  <main>
    <div id="dashboard" class="dashboard-grid">
      <!-- Widgets will be dynamically inserted here -->
    </div>
  </main>
  
  <div id="notifications" class="notifications-container"></div>
  
  <footer>
    <p>Data refreshes automatically. Last updated: <span id="last-updated">Never</span></p>
  </footer>
  
  <script src="js/utils/async.js" type="module"></script>
  <script src="js/errors/index.js" type="module"></script>
  <script src="js/data/sources.js" type="module"></script>
  <script src="js/data/processor.js" type="module"></script>
  <script src="js/visualization/chart.js" type="module"></script>
  <script src="js/DashboardController.js" type="module"></script>
  <script src="js/app.js" type="module"></script>
</body>
</html>
          

CSS Styling

Here's some basic CSS to style your dashboard:

CSS Styling


/* styles.css */
:root {
  --primary-color: #4285F4;
  --secondary-color: #34A853;
  --error-color: #EA4335;
  --warning-color: #FBBC05;
  --text-color: #202124;
  --light-gray: #F1F3F4;
  --dark-gray: #5F6368;
  --border-color: #DADCE0;
  --shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Roboto', 'Segoe UI', Arial, sans-serif;
  color: var(--text-color);
  line-height: 1.6;
  background-color: #fafafa;
}

header {
  background-color: white;
  padding: 1rem 2rem;
  box-shadow: var(--shadow);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

header h1 {
  font-size: 1.5rem;
  font-weight: 500;
}

.controls {
  display: flex;
  gap: 0.5rem;
}

main {
  padding: 2rem;
}

.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-gap: 1rem;
  margin-bottom: 2rem;
}

.widget {
  background-color: white;
  border-radius: 8px;
  box-shadow: var(--shadow);
  padding: 1rem;
  display: flex;
  flex-direction: column;
}

.widget-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.5rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border-color);
}

.widget-title {
  font-size: 1rem;
  font-weight: 500;
}

.widget-actions {
  display: flex;
  gap: 0.5rem;
}

.widget-actions button {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1rem;
  color: var(--dark-gray);
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.widget-actions button:hover {
  background-color: var(--light-gray);
}

.widget-content {
  flex: 1;
  min-height: 200px;
  position: relative;
}

.widget-footer {
  font-size: 0.75rem;
  color: var(--dark-gray);
  padding-top: 0.5rem;
  border-top: 1px solid var(--border-color);
  text-align: right;
}

.btn {
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  font-weight: 500;
}

.btn-primary {
  background-color: var(--primary-color);
  color: white;
}

.btn-danger {
  background-color: var(--error-color);
  color: white;
}

.btn-secondary {
  background-color: var(--light-gray);
  color: var(--text-color);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th,
.data-table td {
  padding: 0.5rem;
  text-align: left;
  border-bottom: 1px solid var(--border-color);
}

.data-table th {
  font-weight: 500;
  background-color: var(--light-gray);
}

.metrics-container {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  height: 100%;
  align-items: center;
}

.metric {
  text-align: center;
  flex: 1;
  min-width: 120px;
  padding: 0.5rem;
}

.metric-value {
  font-size: 2rem;
  font-weight: 500;
}

.metric-value[data-trend="up"] {
  color: var(--secondary-color);
}

.metric-value[data-trend="down"] {
  color: var(--error-color);
}

.metric-label {
  font-size: 0.875rem;
  color: var(--dark-gray);
}

.notifications-container {
  position: fixed;
  top: 1rem;
  right: 1rem;
  width: 300px;
  z-index: 1000;
}

.notification {
  margin-bottom: 0.5rem;
  padding: 1rem;
  border-radius: 4px;
  box-shadow: var(--shadow);
  animation: slideIn 0.3s ease;
}

.notification.error {
  background-color: #FFEBEE;
  border-left: 4px solid var(--error-color);
}

.notification.warning {
  background-color: #FFF8E1;
  border-left: 4px solid var(--warning-color);
}

.notification-title {
  font-weight: 500;
  margin-bottom: 0.25rem;
}

.notification-details {
  font-size: 0.75rem;
  margin-top: 0.5rem;
  color: var(--dark-gray);
}

.notification-close {
  float: right;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.25rem;
  color: var(--dark-gray);
}

.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background-color: white;
  border-radius: 8px;
  box-shadow: var(--shadow);
  width: 100%;
  max-width: 500px;
  overflow: hidden;
}

.modal-header {
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.modal-header h2 {
  font-size: 1.25rem;
  font-weight: 500;
}

.modal-close {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.5rem;
  color: var(--dark-gray);
}

.modal-body {
  padding: 1rem;
}

.form-group {
  margin-bottom: 1rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.25rem;
  font-weight: 500;
}

.form-group input,
.form-group select {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid var(--border-color);
  border-radius: 4px;
}

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1rem;
}

footer {
  padding: 1rem 2rem;
  text-align: center;
  color: var(--dark-gray);
  font-size: 0.875rem;
}

@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}
          

Learning Objectives and Assessment

This weekend project is designed to solidify your understanding of the asynchronous JavaScript concepts covered this week. Here's what you should focus on:

Key Learning Objectives

Assessment Criteria

Your project will be assessed based on:

Documentation Requirements

In addition to your code, please include:

Conclusion

This weekend project brings together all the asynchronous JavaScript concepts you've learned throughout the week. By building a real-time data dashboard, you'll gain hands-on experience with:

Most importantly, you'll learn to apply a structured problem-solving approach based on Polya's four steps: understanding the problem, devising a plan, carrying out the plan, and looking back to reflect and improve.

Remember that complex problems become manageable when broken down into smaller, well-defined components. Take your time to understand each part of the system before implementing it, and don't hesitate to revise your approach as you gain new insights during development.

Good luck, and enjoy building your real-time data dashboard!

Resources and References