import { ErrorBoundary } from "react-error-boundary"; import { Text, Title } from "../../tremor/Text"; import { Button } from "../../tremor/Button"; import { TableChart } from "../TableChart"; import React, { useMemo, useRef, useState } from "react"; import { Chart } from "react-chartjs-2"; import { Chart as ChartJS, Colors } from "chart.js"; import "chart.js/auto"; import { useTheme } from "../../layouts/Dashboard/useTheme"; import "chartjs-adapter-spacetime"; import { MetricChart, MetricPlugin } from "../MetricChart"; import TextChart from "./TextChart"; import ImageChart from "./ImageChart"; import { DividerChart, DividerPlugin } from "./DividerChart"; import { FunnelController, TrapezoidElement, } from "chartjs-chart-funnel"; import ChartDataLabels from "chartjs-plugin-datalabels"; import zoomPlugin from "chartjs-plugin-zoom"; import { PolygonController, PolylineController, MarkerController, MapPlugin, MapController } from "@onvo-ai/chartjs-chart-map"; import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; import { WordCloudController, WordElement } from 'chartjs-chart-wordcloud'; import { SankeyController, Flow } from 'chartjs-chart-sankey'; import { useDashboard } from "../../layouts/Dashboard/useDashboard"; import { useBackend } from "../../layouts"; function transparentize(hexColor: string, opacity: number) { // Convert opacity to hex value let alphaHex = Math.round(opacity * 255).toString(16); if (alphaHex.length === 1) { alphaHex = "0" + alphaHex; } // Return the hex color with the alpha value return hexColor + alphaHex; } function convertFunctionStringsToFunctions(config: any): any { // Helper function to handle each item in the config const convertItem = (item: any): any => { // If it's a string that looks like a function definition if (typeof item === 'string' && item.startsWith('function(')) { // Use the Function constructor to create an actual function try { // Extract the function body and parameters const funcBody = item.substring(item.indexOf('{') + 1, item.lastIndexOf('}')); const funcParams = item.substring(item.indexOf('(') + 1, item.indexOf(')')).split(','); // Create a new function using the Function constructor return new Function(...funcParams, funcBody); } catch (error) { console.error('Error converting function string to function:', error); return item; // Return original if conversion fails } } else if (Array.isArray(item)) { // If it's an array, process each element return item.map(convertItem); } else if (item !== null && typeof item === 'object' && typeof item !== 'function') { // If it's an object (and not a function itself), process each property const result: Record = {}; for (const key in item) { if (Object.prototype.hasOwnProperty.call(item, key)) { result[key] = convertItem(item[key]); } } return result; } // Return unchanged for all other types (including actual functions) return item; }; // Start the conversion by directly passing config to convertItem. // convertItem itself will build a new, transformed object structure, // preserving actual functions and converting stringified ones. // This removes the problematic JSON.parse(JSON.stringify(config)) which stripped functions. return convertItem(config); } ChartJS.register( PolygonController, PolylineController, MarkerController, MapController, FunnelController, TrapezoidElement, MetricChart, MetricPlugin, TextChart, ImageChart, DividerPlugin, DividerChart, ChartDataLabels, zoomPlugin, WordCloudController, WordElement, SankeyController, Flow, MatrixController, MatrixElement, Colors ); export interface ChartBaseProps { json: any; title: string; pagination?: { currentPage: number; pageSize: number; sortColumn?: string; sortDirection?: "ASC" | "DESC"; }; stats?: { totalPages: number; }; setPagination?: React.Dispatch< React.SetStateAction<{ currentPage: number; pageSize: number; sortColumn?: string; sortDirection?: "ASC" | "DESC"; }> >; padding?: number; } const ChartBase: React.FC = ({ json, title, pagination, stats, setPagination, padding }) => { const chartRef = useRef(); const [zoomed, setZoomed] = useState(false); const theme = useTheme(); const { dashboard } = useDashboard(); const { googleMapsLoaded } = useBackend(); const resetZoom = () => { chartRef.current?.resetZoom(); setZoomed(false); }; let chartConfig = useMemo(() => { if (!json) { return undefined; } let output = Object.assign({}, json, {}); if (!output.options) { output.options = {}; } if (!output.options.plugins) { output.options.plugins = {}; } let subtitle = output.options.plugins?.subtitle?.display !== false && output.options.plugins?.subtitle?.text?.trim && output.options.plugins?.subtitle?.text?.trim() !== ""; output.options.plugins.title = { display: (title || output.options.plugins.title?.text || "").trim() === "" ? false : true, text: title || output.options.plugins.title?.text || "", align: output.options.plugins.title?.align || "start", position: output.options.plugins.title?.position || "top", fullSize: true, font: { size: output.type === "text" ? 24 : 18, weight: 600, }, color: theme === "dark" ? (dashboard?.settings?.dark_text || "#ddd") : (dashboard?.settings?.light_text || "#111"), padding: output.options.plugins.title?.padding || { bottom: 5, }, }; if (subtitle) { output.options.plugins.subtitle = { display: output.options.plugins?.subtitle?.display || true, text: output.options.plugins.subtitle?.text || "", align: output.options.plugins.subtitle?.align || "start", position: output.options.plugins.subtitle?.position || "top", fullSize: true, font: { size: 12, weight: 400, }, padding: output.options.plugins.subtitle?.padding || { bottom: 10, }, }; } if (["line", "bar", "scatter"].indexOf(json.type) >= 0 && !dashboard?.settings?.disable_widget_interactions) { output.options.plugins.zoom = { pan: { enabled: true, mode: output.type === "scatter" ? "xy" : "x", modifierKey: "shift", }, zoom: { drag: { enabled: true, backgroundColor: "rgba(59, 130, 246, 0.2)", borderColor: "rgb(59, 130, 246)", borderWidth: 2, }, pinch: { enabled: true, }, mode: output.type === "scatter" ? "xy" : "x", onZoomComplete: ({ chart }: { chart: any }) => { setZoomed(true); }, on: (e: any) => { e.stopPropagation(); e.preventDefault(); } }, }; } if (output.type === "matrix") { output.data.datasets = output.data.datasets.map((dataset: any, index: number) => { return { ...dataset, backgroundColor: (context: any) => { const value = context.dataset.data[context.dataIndex].v; const max = Math.max(...dataset.data.map((d: any) => d.v)); const opacity = (value / max); var alpha = opacity === undefined ? 0.5 : (1 - opacity); return transparentize(dataset.baseColor || "#22c55e", alpha); }, borderWidth: 2, borderColor: theme === "dark" ? "#0f172a" : "#ffffff", width: ({ chart }: { chart: any }) => (chart.chartArea || {}).width / chart.scales.x.ticks.length, height: ({ chart }: { chart: any }) => (chart.chartArea || {}).height / chart.scales.y.ticks.length }; }); } output.options.theme = theme; if (!output.options.layout) { output.options.layout = {}; } output.options.layout.padding = padding; return output; }, [json, title, theme, dashboard]); const options = useMemo(() => chartConfig ? convertFunctionStringsToFunctions(chartConfig.options) : undefined, [chartConfig]); if (chartConfig && chartConfig.type === "map" && !googleMapsLoaded) { return <> } return ( (
Error rendering chart {error.message}
)} > {chartConfig ? ( chartConfig.type === "table" ? ( ) : (
Hold{" "} Shift {" "} and drag to pan
) ) : ( <> )}
); }; export default ChartBase;