From 2c30fa0521feadad5005d47df461fb648e998f58 Mon Sep 17 00:00:00 2001 From: alphaeusmote <41258468-alphaeusmote@users.noreply.replit.com> Date: Thu, 10 Apr 2025 00:56:22 +0000 Subject: [PATCH] Enhance dashboard widgets with advanced chart options Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/27b747f7-6777-469c-b2a8-4352fda5d8eb.jpg --- .../components/dashboard/dashboard-layout.tsx | 19 +- .../dashboard/dashboard-widget-fixed.tsx | 430 ++++++++++++++++++ .../components/dashboard/dashboard-widget.tsx | 154 ++++++- 3 files changed, 584 insertions(+), 19 deletions(-) create mode 100644 client/src/components/dashboard/dashboard-widget-fixed.tsx diff --git a/client/src/components/dashboard/dashboard-layout.tsx b/client/src/components/dashboard/dashboard-layout.tsx index 16f58d4..666f9e8 100644 --- a/client/src/components/dashboard/dashboard-layout.tsx +++ b/client/src/components/dashboard/dashboard-layout.tsx @@ -93,6 +93,15 @@ export function DashboardLayout({ showLegend: widget.config.showLegend ?? true, stacked: widget.config.stacked ?? false, precision: widget.config.precision ?? 2, + // Add new chart options with defaults + colors: widget.config.colors || [], + showGrid: widget.config.showGrid ?? true, + showTooltip: widget.config.showTooltip ?? true, + enableAnimation: widget.config.enableAnimation ?? true, + valueFormatter: widget.config.valueFormatter ?? "none", + currencySymbol: widget.config.currencySymbol ?? "$", + minValue: widget.config.minValue ?? null, + maxValue: widget.config.maxValue ?? null, }, }; @@ -146,7 +155,15 @@ export function DashboardLayout({ showLegend: values.config.showLegend, stacked: values.config.stacked, precision: values.config.precision, - colors: ['#8884d8', '#82ca9d', '#ffc658', '#ff8042', '#0088fe'], + colors: values.config.colors || ['#8884d8', '#82ca9d', '#ffc658', '#ff8042', '#0088fe'], + // New chart options + showGrid: values.config.showGrid, + showTooltip: values.config.showTooltip, + enableAnimation: values.config.enableAnimation, + valueFormatter: values.config.valueFormatter, + currencySymbol: values.config.currencySymbol, + minValue: values.config.minValue, + maxValue: values.config.maxValue, }, onEdit: handleEditWidget, onDelete: handleDeleteWidget, diff --git a/client/src/components/dashboard/dashboard-widget-fixed.tsx b/client/src/components/dashboard/dashboard-widget-fixed.tsx new file mode 100644 index 0000000..b46cae5 --- /dev/null +++ b/client/src/components/dashboard/dashboard-widget-fixed.tsx @@ -0,0 +1,430 @@ +import { useState, useEffect, useRef } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Settings, Trash2, Maximize2, Download } from "lucide-react"; +import { AreaChart, Area, BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"; +import { exportToExcel, exportToPDF, exportToCSV } from "../../lib/export-utils"; + +// Type for dataKey to work around TypeScript restrictions +type DataKeyType = string | number | ((obj: any) => any); + +export interface DataPoint { + [key: string]: any; +} + +export interface WidgetProps { + id: string; + title: string; + type: "bar" | "line" | "pie" | "area" | "table" | "number"; + data: DataPoint[]; + config: { + xAxis?: string; + yAxis?: string | string[]; + dimensions?: string[]; + metrics?: string[]; + colors?: string[]; + showLegend?: boolean; + stacked?: boolean; + precision?: number; + // New chart options + showGrid?: boolean; + showTooltip?: boolean; + enableAnimation?: boolean; + valueFormatter?: "none" | "number" | "percent" | "currency"; + currencySymbol?: string; + minValue?: number | null; + maxValue?: number | null; + }; + onEdit: (id: string) => void; + onDelete: (id: string) => void; +} + +const COLORS = [ + "#8884d8", "#82ca9d", "#ffc658", "#ff8042", "#0088fe", + "#00C49F", "#FFBB28", "#FF8042", "#a4de6c", "#d0ed57" +]; + +export function DashboardWidget({ id, title, type, data, config, onEdit, onDelete }: WidgetProps) { + const [expanded, setExpanded] = useState(false); + const contentRef = useRef(null); + + // Format data for number type + const calculateNumber = () => { + if (!data || data.length === 0 || !config.metrics || config.metrics.length === 0) return 0; + + const metric = config.metrics[0]; + let value = 0; + + // Sum all values for the metric + data.forEach(item => { + if (item[metric] !== undefined && !isNaN(Number(item[metric]))) { + value += Number(item[metric]); + } + }); + + // Apply precision if specified + if (config.precision !== undefined) { + return value.toFixed(config.precision); + } + + return value; + }; + + const handleExport = (format: 'excel' | 'pdf' | 'csv') => { + switch (format) { + case 'excel': + exportToExcel(data, title); + break; + case 'pdf': + exportToPDF(data, title, config); + break; + case 'csv': + exportToCSV(data, title); + break; + } + }; + + // Render a table + const renderTable = () => { + if (!data || data.length === 0) return

No data available

; + + // Determine columns from first data point and config + const columns = Object.keys(data[0]) + .filter(key => { + if (config.dimensions && config.metrics) { + return [...config.dimensions, ...config.metrics].includes(key); + } + return true; + }); + + return ( +
+ + + + {columns.map((column) => ( + + ))} + + + + {data.map((item, idx) => ( + + {columns.map((column) => ( + + ))} + + ))} + +
+ {column} +
+ {item[column] !== undefined ? String(item[column]) : '—'} +
+
+ ); + }; + + // Render different chart types + const renderChart = () => { + if (!data || data.length === 0) return

No data available

; + + const { + xAxis, + yAxis, + dimensions, + metrics, + colors = COLORS, + showLegend = true, + stacked = false, + // New chart options with defaults + showGrid = true, + showTooltip = true, + enableAnimation = true, + valueFormatter = "none", + currencySymbol = "$", + minValue = null, + maxValue = null + } = config; + + const xAxisKey = xAxis as string; + + // Format values based on valueFormatter setting + const formatValue = (value: any, index?: number): string => { + const numValue = Number(value); + + if (valueFormatter === "none" || isNaN(numValue)) return String(value); + + if (valueFormatter === "number") { + return numValue.toLocaleString(); + } + + if (valueFormatter === "percent") { + return `${(numValue * 100).toFixed(config.precision || 0)}%`; + } + + if (valueFormatter === "currency") { + return `${currencySymbol}${numValue.toLocaleString(undefined, { + minimumFractionDigits: config.precision || 0, + maximumFractionDigits: config.precision || 0 + })}`; + } + + return String(value); + }; + + // Custom tooltip formatter + const customTooltipFormatter = (value: number, name: string) => { + return [formatValue(value), name]; + }; + + // TypeScript-compatible tickFormatter for YAxis + const yAxisTickFormatter = (value: any) => { + return valueFormatter !== "none" ? String(formatValue(value)) : String(value); + }; + + switch (type) { + case 'bar': + return ( + + + {showGrid && } + + + {showTooltip && } + {showLegend && } + {Array.isArray(yAxis) && yAxis.length > 0 + ? yAxis.map((axis, index) => ( + + )) + : yAxis ? : null + } + + + ); + + case 'line': + return ( + + + {showGrid && } + + + {showTooltip && } + {showLegend && } + {Array.isArray(yAxis) && yAxis.length > 0 + ? yAxis.map((axis, index) => ( + + )) + : yAxis ? : null + } + + + ); + + case 'area': + return ( + + + {showGrid && } + + + {showTooltip && } + {showLegend && } + {Array.isArray(yAxis) && yAxis.length > 0 + ? yAxis.map((axis, index) => ( + + )) + : yAxis ? : null + } + + + ); + + case 'pie': + // For pie charts, we need to process the data differently + const pieData = data.map(item => { + const dimensionField = dimensions?.[0] || xAxis || "name"; + const metricField = Array.isArray(metrics) && metrics.length > 0 ? metrics[0] : + (Array.isArray(yAxis) && yAxis.length > 0 ? yAxis[0] : + (typeof yAxis === 'string' ? yAxis : "value")); + + const name = item[dimensionField as string]; + const value = item[metricField as string]; + return { name, value: Number(value) }; + }).filter(item => !isNaN(item.value)); + + return ( + + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={80} + fill="#8884d8" + dataKey="value" + isAnimationActive={enableAnimation} + > + {pieData.map((entry, index) => ( + + ))} + + {showTooltip && [formatValue(Number(value)), ""]} + />} + {showLegend && } + + + ); + + case 'number': + const rawValue = calculateNumber(); + let displayValue = rawValue; + + // Format value based on valueFormatter + if (valueFormatter === "number") { + displayValue = Number(rawValue).toLocaleString(undefined, { + minimumFractionDigits: config.precision || 0, + maximumFractionDigits: config.precision || 0 + }); + } else if (valueFormatter === "percent") { + displayValue = `${(Number(rawValue) * 100).toFixed(config.precision || 0)}%`; + } else if (valueFormatter === "currency") { + displayValue = `${currencySymbol}${Number(rawValue).toLocaleString(undefined, { + minimumFractionDigits: config.precision || 0, + maximumFractionDigits: config.precision || 0 + })}`; + } + + return ( +
+
+

{displayValue}

+ {config.metrics && config.metrics.length > 0 && ( +

{config.metrics[0]}

+ )} +
+
+ ); + + case 'table': + default: + return renderTable(); + } + }; + + return ( + + + {title} +
+ + + + +
+
+ + {renderChart()} + +
+ ); +} \ No newline at end of file diff --git a/client/src/components/dashboard/dashboard-widget.tsx b/client/src/components/dashboard/dashboard-widget.tsx index 7f7756d..b46cae5 100644 --- a/client/src/components/dashboard/dashboard-widget.tsx +++ b/client/src/components/dashboard/dashboard-widget.tsx @@ -132,18 +132,78 @@ export function DashboardWidget({ id, title, type, data, config, onEdit, onDelet const renderChart = () => { if (!data || data.length === 0) return

No data available

; - const { xAxis, yAxis, dimensions, metrics, colors = COLORS, showLegend = true, stacked = false } = config; + const { + xAxis, + yAxis, + dimensions, + metrics, + colors = COLORS, + showLegend = true, + stacked = false, + // New chart options with defaults + showGrid = true, + showTooltip = true, + enableAnimation = true, + valueFormatter = "none", + currencySymbol = "$", + minValue = null, + maxValue = null + } = config; + const xAxisKey = xAxis as string; + // Format values based on valueFormatter setting + const formatValue = (value: any, index?: number): string => { + const numValue = Number(value); + + if (valueFormatter === "none" || isNaN(numValue)) return String(value); + + if (valueFormatter === "number") { + return numValue.toLocaleString(); + } + + if (valueFormatter === "percent") { + return `${(numValue * 100).toFixed(config.precision || 0)}%`; + } + + if (valueFormatter === "currency") { + return `${currencySymbol}${numValue.toLocaleString(undefined, { + minimumFractionDigits: config.precision || 0, + maximumFractionDigits: config.precision || 0 + })}`; + } + + return String(value); + }; + + // Custom tooltip formatter + const customTooltipFormatter = (value: number, name: string) => { + return [formatValue(value), name]; + }; + + // TypeScript-compatible tickFormatter for YAxis + const yAxisTickFormatter = (value: any) => { + return valueFormatter !== "none" ? String(formatValue(value)) : String(value); + }; + switch (type) { case 'bar': return ( - - + + {showGrid && } - - + + {showTooltip && } {showLegend && } {Array.isArray(yAxis) && yAxis.length > 0 ? yAxis.map((axis, index) => ( @@ -152,9 +212,14 @@ export function DashboardWidget({ id, title, type, data, config, onEdit, onDelet dataKey={axis as string} stackId={stacked ? "a" : undefined} fill={colors[index % colors.length]} + isAnimationActive={enableAnimation} /> )) - : yAxis ? : null + : yAxis ? : null } @@ -163,11 +228,20 @@ export function DashboardWidget({ id, title, type, data, config, onEdit, onDelet case 'line': return ( - - + + {showGrid && } - - + + {showTooltip && } {showLegend && } {Array.isArray(yAxis) && yAxis.length > 0 ? yAxis.map((axis, index) => ( @@ -177,9 +251,16 @@ export function DashboardWidget({ id, title, type, data, config, onEdit, onDelet dataKey={axis as string} stroke={colors[index % colors.length]} activeDot={{ r: 8 }} + isAnimationActive={enableAnimation} /> )) - : yAxis ? : null + : yAxis ? : null } @@ -188,11 +269,20 @@ export function DashboardWidget({ id, title, type, data, config, onEdit, onDelet case 'area': return ( - - + + {showGrid && } - - + + {showTooltip && } {showLegend && } {Array.isArray(yAxis) && yAxis.length > 0 ? yAxis.map((axis, index) => ( @@ -203,9 +293,16 @@ export function DashboardWidget({ id, title, type, data, config, onEdit, onDelet stackId={stacked ? "a" : `${index}`} fill={colors[index % colors.length]} stroke={colors[index % colors.length]} + isAnimationActive={enableAnimation} /> )) - : yAxis ? : null + : yAxis ? : null } @@ -236,22 +333,43 @@ export function DashboardWidget({ id, title, type, data, config, onEdit, onDelet outerRadius={80} fill="#8884d8" dataKey="value" + isAnimationActive={enableAnimation} > {pieData.map((entry, index) => ( ))} - [`${value}`, ""]} /> + {showTooltip && [formatValue(Number(value)), ""]} + />} {showLegend && } ); case 'number': + const rawValue = calculateNumber(); + let displayValue = rawValue; + + // Format value based on valueFormatter + if (valueFormatter === "number") { + displayValue = Number(rawValue).toLocaleString(undefined, { + minimumFractionDigits: config.precision || 0, + maximumFractionDigits: config.precision || 0 + }); + } else if (valueFormatter === "percent") { + displayValue = `${(Number(rawValue) * 100).toFixed(config.precision || 0)}%`; + } else if (valueFormatter === "currency") { + displayValue = `${currencySymbol}${Number(rawValue).toLocaleString(undefined, { + minimumFractionDigits: config.precision || 0, + maximumFractionDigits: config.precision || 0 + })}`; + } + return (
-

{calculateNumber()}

+

{displayValue}

{config.metrics && config.metrics.length > 0 && (

{config.metrics[0]}

)}