PrithviGuardian / client /src /lib /chartVisualizer.ts
Varad Bakshi
Fresh upload
e7427b5
Raw
History Blame Contribute Delete
20 kB
// 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 };