Spaces:
Configuration error
Configuration error
| // Graph-based Trend Visualization with Offline Charts | |
| // Comprehensive charting system using Chart.js for environmental data visualization | |
| import { Chart, ChartConfiguration, ChartData, ChartOptions } from 'chart.js'; | |
| interface ChartDataPoint { | |
| x: number | string | Date; | |
| y: number; | |
| label?: string; | |
| metadata?: Record<string, any>; | |
| } | |
| interface TrendAnalysis { | |
| slope: number; | |
| correlation: number; | |
| trend: 'increasing' | 'decreasing' | 'stable'; | |
| confidence: number; | |
| forecast?: ChartDataPoint[]; | |
| } | |
| interface ChartTheme { | |
| primary: string; | |
| secondary: string; | |
| accent: string; | |
| background: string; | |
| text: string; | |
| grid: string; | |
| } | |
| class ChartVisualizer { | |
| private readonly indianTheme: ChartTheme = { | |
| primary: '#FF6B35', // Saffron | |
| secondary: '#2E8B57', // Sea Green | |
| accent: '#4169E1', // Royal Blue | |
| background: '#FFFEF7', // Cream | |
| text: '#2F4F4F', // Dark Slate Gray | |
| grid: '#E6E6FA' // Lavender | |
| }; | |
| // Time Series Chart for Environmental Parameters | |
| createTimeSeriesChart( | |
| canvasId: string, | |
| data: ChartDataPoint[], | |
| options: { | |
| title: string; | |
| yAxisLabel: string; | |
| parameter: string; | |
| unit: string; | |
| showTrend?: boolean; | |
| thresholds?: { value: number; label: string; color: string }[]; | |
| } | |
| ): Chart { | |
| try { | |
| if (!data || data.length === 0) { | |
| throw new Error('Chart data is required and cannot be empty'); | |
| } | |
| this.validateChartData(data); | |
| const canvas = document.getElementById(canvasId) as HTMLCanvasElement; | |
| if (!canvas) { | |
| throw new Error(`Canvas element with ID '${canvasId}' not found`); | |
| } | |
| // Sort data by time | |
| const sortedData = [...data].sort((a, b) => { | |
| const timeA = new Date(a.x).getTime(); | |
| const timeB = new Date(b.x).getTime(); | |
| return timeA - timeB; | |
| }); | |
| const datasets: any[] = [{ | |
| label: `${options.parameter} (${options.unit})`, | |
| data: sortedData.map(point => ({ x: point.x, y: point.y })), | |
| borderColor: this.indianTheme.primary, | |
| backgroundColor: this.indianTheme.primary + '20', | |
| tension: 0.4, | |
| fill: true, | |
| pointBackgroundColor: this.indianTheme.primary, | |
| pointBorderColor: '#fff', | |
| pointBorderWidth: 2, | |
| pointRadius: 4 | |
| }]; | |
| // Add trend line if requested | |
| if (options.showTrend && sortedData.length > 2) { | |
| const trendData = this.calculateTrendLine(sortedData); | |
| datasets.push({ | |
| label: 'Trend', | |
| data: trendData, | |
| borderColor: this.indianTheme.accent, | |
| backgroundColor: 'transparent', | |
| borderDash: [5, 5], | |
| pointRadius: 0, | |
| tension: 0 | |
| }); | |
| } | |
| // Add threshold lines | |
| if (options.thresholds) { | |
| options.thresholds.forEach((threshold, index) => { | |
| datasets.push({ | |
| label: threshold.label, | |
| data: sortedData.map(point => ({ x: point.x, y: threshold.value })), | |
| borderColor: threshold.color, | |
| backgroundColor: 'transparent', | |
| borderDash: [10, 5], | |
| pointRadius: 0, | |
| tension: 0 | |
| }); | |
| }); | |
| } | |
| const config: ChartConfiguration = { | |
| type: 'line', | |
| data: { datasets }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: options.title, | |
| font: { size: 16, weight: 'bold' }, | |
| color: this.indianTheme.text | |
| }, | |
| legend: { | |
| display: true, | |
| position: 'top', | |
| labels: { | |
| color: this.indianTheme.text, | |
| usePointStyle: true | |
| } | |
| }, | |
| tooltip: { | |
| mode: 'index', | |
| intersect: false, | |
| backgroundColor: this.indianTheme.background, | |
| titleColor: this.indianTheme.text, | |
| bodyColor: this.indianTheme.text, | |
| borderColor: this.indianTheme.primary, | |
| borderWidth: 1 | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| type: 'time', | |
| display: true, | |
| title: { | |
| display: true, | |
| text: 'Time', | |
| color: this.indianTheme.text | |
| }, | |
| grid: { | |
| color: this.indianTheme.grid | |
| }, | |
| ticks: { | |
| color: this.indianTheme.text | |
| } | |
| }, | |
| y: { | |
| display: true, | |
| title: { | |
| display: true, | |
| text: options.yAxisLabel, | |
| color: this.indianTheme.text | |
| }, | |
| grid: { | |
| color: this.indianTheme.grid | |
| }, | |
| ticks: { | |
| color: this.indianTheme.text | |
| } | |
| } | |
| }, | |
| interaction: { | |
| mode: 'nearest', | |
| axis: 'x', | |
| intersect: false | |
| } | |
| } | |
| }; | |
| return new Chart(canvas, config); | |
| } catch (error) { | |
| throw new Error(`Failed to create time series chart: ${error instanceof Error ? error.message : 'Unknown error'}`); | |
| } | |
| } | |
| // Bar Chart for Comparative Analysis | |
| createBarChart( | |
| canvasId: string, | |
| data: { label: string; value: number; color?: string }[], | |
| options: { | |
| title: string; | |
| yAxisLabel: string; | |
| horizontal?: boolean; | |
| } | |
| ): Chart { | |
| try { | |
| if (!data || data.length === 0) { | |
| throw new Error('Chart data is required and cannot be empty'); | |
| } | |
| const canvas = document.getElementById(canvasId) as HTMLCanvasElement; | |
| if (!canvas) { | |
| throw new Error(`Canvas element with ID '${canvasId}' not found`); | |
| } | |
| const colors = data.map((item, index) => | |
| item.color || this.getColorByIndex(index) | |
| ); | |
| const config: ChartConfiguration = { | |
| type: options.horizontal ? 'bar' : 'bar', | |
| data: { | |
| labels: data.map(item => item.label), | |
| datasets: [{ | |
| label: options.yAxisLabel, | |
| data: data.map(item => item.value), | |
| backgroundColor: colors.map(color => color + '80'), | |
| borderColor: colors, | |
| borderWidth: 2 | |
| }] | |
| }, | |
| options: { | |
| indexAxis: options.horizontal ? 'y' : 'x', | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: options.title, | |
| font: { size: 16, weight: 'bold' }, | |
| color: this.indianTheme.text | |
| }, | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| backgroundColor: this.indianTheme.background, | |
| titleColor: this.indianTheme.text, | |
| bodyColor: this.indianTheme.text, | |
| borderColor: this.indianTheme.primary, | |
| borderWidth: 1 | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| grid: { | |
| color: this.indianTheme.grid | |
| }, | |
| ticks: { | |
| color: this.indianTheme.text | |
| } | |
| }, | |
| y: { | |
| grid: { | |
| color: this.indianTheme.grid | |
| }, | |
| ticks: { | |
| color: this.indianTheme.text | |
| }, | |
| title: { | |
| display: true, | |
| text: options.yAxisLabel, | |
| color: this.indianTheme.text | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| return new Chart(canvas, config); | |
| } catch (error) { | |
| throw new Error(`Failed to create bar chart: ${error instanceof Error ? error.message : 'Unknown error'}`); | |
| } | |
| } | |
| // Scatter Plot for Correlation Analysis | |
| createScatterPlot( | |
| canvasId: string, | |
| data: { x: number; y: number; label?: string }[], | |
| options: { | |
| title: string; | |
| xAxisLabel: string; | |
| yAxisLabel: string; | |
| showTrendLine?: boolean; | |
| } | |
| ): Chart { | |
| try { | |
| if (!data || data.length === 0) { | |
| throw new Error('Chart data is required and cannot be empty'); | |
| } | |
| data.forEach((point, index) => { | |
| if (typeof point.x !== 'number' || typeof point.y !== 'number') { | |
| throw new Error(`Invalid data point at index ${index}: x and y must be numbers`); | |
| } | |
| if (isNaN(point.x) || isNaN(point.y)) { | |
| throw new Error(`Invalid data point at index ${index}: x and y cannot be NaN`); | |
| } | |
| }); | |
| const canvas = document.getElementById(canvasId) as HTMLCanvasElement; | |
| if (!canvas) { | |
| throw new Error(`Canvas element with ID '${canvasId}' not found`); | |
| } | |
| const datasets: any[] = [{ | |
| label: 'Data Points', | |
| data: data, | |
| backgroundColor: this.indianTheme.primary + '80', | |
| borderColor: this.indianTheme.primary, | |
| pointRadius: 6, | |
| pointHoverRadius: 8 | |
| }]; | |
| // Add trend line if requested | |
| if (options.showTrendLine && data.length > 2) { | |
| const correlation = this.calculateCorrelation(data); | |
| const trendLine = this.calculateLinearRegression(data); | |
| datasets.push({ | |
| label: `Trend Line (r = ${correlation.toFixed(3)})`, | |
| data: trendLine, | |
| type: 'line', | |
| backgroundColor: 'transparent', | |
| borderColor: this.indianTheme.accent, | |
| borderDash: [5, 5], | |
| pointRadius: 0, | |
| tension: 0 | |
| }); | |
| } | |
| const config: ChartConfiguration = { | |
| type: 'scatter', | |
| data: { datasets }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: options.title, | |
| font: { size: 16, weight: 'bold' }, | |
| color: this.indianTheme.text | |
| }, | |
| legend: { | |
| display: true, | |
| position: 'top', | |
| labels: { | |
| color: this.indianTheme.text | |
| } | |
| }, | |
| tooltip: { | |
| backgroundColor: this.indianTheme.background, | |
| titleColor: this.indianTheme.text, | |
| bodyColor: this.indianTheme.text, | |
| borderColor: this.indianTheme.primary, | |
| borderWidth: 1, | |
| callbacks: { | |
| label: function(context: any) { | |
| const point = context.parsed; | |
| return `(${point.x.toFixed(2)}, ${point.y.toFixed(2)})`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| type: 'linear', | |
| position: 'bottom', | |
| title: { | |
| display: true, | |
| text: options.xAxisLabel, | |
| color: this.indianTheme.text | |
| }, | |
| grid: { | |
| color: this.indianTheme.grid | |
| }, | |
| ticks: { | |
| color: this.indianTheme.text | |
| } | |
| }, | |
| y: { | |
| title: { | |
| display: true, | |
| text: options.yAxisLabel, | |
| color: this.indianTheme.text | |
| }, | |
| grid: { | |
| color: this.indianTheme.grid | |
| }, | |
| ticks: { | |
| color: this.indianTheme.text | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| return new Chart(canvas, config); | |
| } catch (error) { | |
| throw new Error(`Failed to create scatter plot: ${error instanceof Error ? error.message : 'Unknown error'}`); | |
| } | |
| } | |
| // Pie Chart for Distribution Analysis | |
| createPieChart( | |
| canvasId: string, | |
| data: { label: string; value: number; color?: string }[], | |
| options: { | |
| title: string; | |
| showPercentages?: boolean; | |
| } | |
| ): Chart { | |
| try { | |
| if (!data || data.length === 0) { | |
| throw new Error('Chart data is required and cannot be empty'); | |
| } | |
| data.forEach((item, index) => { | |
| if (typeof item.value !== 'number' || item.value < 0) { | |
| throw new Error(`Invalid value at index ${index}: must be a non-negative number`); | |
| } | |
| }); | |
| const canvas = document.getElementById(canvasId) as HTMLCanvasElement; | |
| if (!canvas) { | |
| throw new Error(`Canvas element with ID '${canvasId}' not found`); | |
| } | |
| const total = data.reduce((sum, item) => sum + item.value, 0); | |
| if (total === 0) { | |
| throw new Error('Total of all values cannot be zero'); | |
| } | |
| const colors = data.map((item, index) => | |
| item.color || this.getColorByIndex(index) | |
| ); | |
| const config: ChartConfiguration = { | |
| type: 'pie', | |
| data: { | |
| labels: data.map(item => item.label), | |
| datasets: [{ | |
| data: data.map(item => item.value), | |
| backgroundColor: colors.map(color => color + '80'), | |
| borderColor: colors, | |
| borderWidth: 2 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: options.title, | |
| font: { size: 16, weight: 'bold' }, | |
| color: this.indianTheme.text | |
| }, | |
| legend: { | |
| display: true, | |
| position: 'right', | |
| labels: { | |
| color: this.indianTheme.text, | |
| usePointStyle: true | |
| } | |
| }, | |
| tooltip: { | |
| backgroundColor: this.indianTheme.background, | |
| titleColor: this.indianTheme.text, | |
| bodyColor: this.indianTheme.text, | |
| borderColor: this.indianTheme.primary, | |
| borderWidth: 1, | |
| callbacks: { | |
| label: function(context: any) { | |
| const value = context.parsed; | |
| const percentage = ((value / total) * 100).toFixed(1); | |
| return options.showPercentages | |
| ? `${context.label}: ${value} (${percentage}%)` | |
| : `${context.label}: ${value}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| return new Chart(canvas, config); | |
| } catch (error) { | |
| throw new Error(`Failed to create pie chart: ${error instanceof Error ? error.message : 'Unknown error'}`); | |
| } | |
| } | |
| // Trend Analysis | |
| analyzeTrend(data: ChartDataPoint[]): TrendAnalysis { | |
| try { | |
| if (!data || data.length < 3) { | |
| throw new Error('At least 3 data points are required for trend analysis'); | |
| } | |
| this.validateChartData(data); | |
| // Convert to numeric format for analysis | |
| const numericData = data.map((point, index) => ({ | |
| x: index, | |
| y: point.y | |
| })); | |
| const correlation = this.calculateCorrelation(numericData); | |
| const regression = this.calculateLinearRegression(numericData); | |
| const slope = this.calculateSlope(numericData); | |
| let trend: 'increasing' | 'decreasing' | 'stable'; | |
| if (Math.abs(slope) < 0.01) { | |
| trend = 'stable'; | |
| } else if (slope > 0) { | |
| trend = 'increasing'; | |
| } else { | |
| trend = 'decreasing'; | |
| } | |
| // Calculate confidence based on correlation strength | |
| const confidence = Math.min(95, Math.abs(correlation) * 100); | |
| // Generate forecast for next 3 points | |
| const forecast: ChartDataPoint[] = []; | |
| const lastIndex = numericData.length - 1; | |
| for (let i = 1; i <= 3; i++) { | |
| const futureX = lastIndex + i; | |
| const futureY = slope * futureX + this.getYIntercept(numericData); | |
| forecast.push({ | |
| x: futureX, | |
| y: Math.max(0, futureY), // Ensure non-negative forecast | |
| label: `Forecast ${i}` | |
| }); | |
| } | |
| return { | |
| slope, | |
| correlation, | |
| trend, | |
| confidence, | |
| forecast | |
| }; | |
| } catch (error) { | |
| throw new Error(`Failed to analyze trend: ${error instanceof Error ? error.message : 'Unknown error'}`); | |
| } | |
| } | |
| // Export chart as image | |
| exportChart(chart: Chart, filename: string = 'chart'): void { | |
| try { | |
| if (!chart || !chart.canvas) { | |
| throw new Error('Invalid chart object'); | |
| } | |
| const canvas = chart.canvas; | |
| const url = canvas.toDataURL('image/png'); | |
| const link = document.createElement('a'); | |
| link.download = `${filename}.png`; | |
| link.href = url; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } catch (error) { | |
| throw new Error(`Failed to export chart: ${error instanceof Error ? error.message : 'Unknown error'}`); | |
| } | |
| } | |
| // Private helper methods | |
| private validateChartData(data: ChartDataPoint[]): void { | |
| data.forEach((point, index) => { | |
| if (typeof point.y !== 'number') { | |
| throw new Error(`Invalid y value at index ${index}: must be a number`); | |
| } | |
| if (isNaN(point.y) || !isFinite(point.y)) { | |
| throw new Error(`Invalid y value at index ${index}: must be a finite number`); | |
| } | |
| }); | |
| } | |
| private calculateTrendLine(data: ChartDataPoint[]): { x: string | number | Date; y: number }[] { | |
| const numericData = data.map((point, index) => ({ x: index, y: point.y })); | |
| const regression = this.calculateLinearRegression(numericData); | |
| return [ | |
| { x: data[0].x, y: regression[0].y }, | |
| { x: data[data.length - 1].x, y: regression[regression.length - 1].y } | |
| ]; | |
| } | |
| private calculateCorrelation(data: { x: number; y: number }[]): number { | |
| const n = data.length; | |
| const sumX = data.reduce((sum, point) => sum + point.x, 0); | |
| const sumY = data.reduce((sum, point) => sum + point.y, 0); | |
| const sumXY = data.reduce((sum, point) => sum + point.x * point.y, 0); | |
| const sumX2 = data.reduce((sum, point) => sum + point.x * point.x, 0); | |
| const sumY2 = data.reduce((sum, point) => sum + point.y * point.y, 0); | |
| const numerator = n * sumXY - sumX * sumY; | |
| const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); | |
| return denominator === 0 ? 0 : numerator / denominator; | |
| } | |
| private calculateLinearRegression(data: { x: number; y: number }[]): { x: number; y: number }[] { | |
| const slope = this.calculateSlope(data); | |
| const yIntercept = this.getYIntercept(data); | |
| return data.map(point => ({ | |
| x: point.x, | |
| y: slope * point.x + yIntercept | |
| })); | |
| } | |
| private calculateSlope(data: { x: number; y: number }[]): number { | |
| const n = data.length; | |
| const sumX = data.reduce((sum, point) => sum + point.x, 0); | |
| const sumY = data.reduce((sum, point) => sum + point.y, 0); | |
| const sumXY = data.reduce((sum, point) => sum + point.x * point.y, 0); | |
| const sumX2 = data.reduce((sum, point) => sum + point.x * point.x, 0); | |
| const denominator = n * sumX2 - sumX * sumX; | |
| return denominator === 0 ? 0 : (n * sumXY - sumX * sumY) / denominator; | |
| } | |
| private getYIntercept(data: { x: number; y: number }[]): number { | |
| const n = data.length; | |
| const sumX = data.reduce((sum, point) => sum + point.x, 0); | |
| const sumY = data.reduce((sum, point) => sum + point.y, 0); | |
| const slope = this.calculateSlope(data); | |
| return (sumY - slope * sumX) / n; | |
| } | |
| private getColorByIndex(index: number): string { | |
| const colors = [ | |
| this.indianTheme.primary, | |
| this.indianTheme.secondary, | |
| this.indianTheme.accent, | |
| '#FF4500', // Orange Red | |
| '#32CD32', // Lime Green | |
| '#6A5ACD', // Slate Blue | |
| '#FF69B4', // Hot Pink | |
| '#20B2AA', // Light Sea Green | |
| '#FFD700', // Gold | |
| '#DC143C' // Crimson | |
| ]; | |
| return colors[index % colors.length]; | |
| } | |
| } | |
| export const chartVisualizer = new ChartVisualizer(); | |
| export { ChartDataPoint, TrendAnalysis, ChartTheme }; |