import React, { useEffect, useMemo, useRef, useCallback, useState } from 'react'; import { observer } from 'mobx-react-lite'; import { useTranslation } from 'react-i18next'; import { ISegmentKey, IAppI18nProps, IVizProps, IErrorHandlerProps, IVizAppProps, ISpecProps, IComputationContextProps, IComputationProps, IThemeKey, } from './interfaces'; import { GWGlobalConfig } from './vis/theme'; import type { IReactVegaHandler } from './vis/react-vega'; import VisualSettings from './visualSettings'; import PosFields from './fields/posFields'; import AestheticFields from './fields/aestheticFields'; import DatasetFields from './fields/datasetFields/index'; import ReactiveRenderer from './renderer/index'; import { VizStoreWrapper, useVizStore, withErrorReport, withTimeout } from './store'; import VisNav from './segments/visNav'; import { mergeLocaleRes, setLocaleLanguage } from './locales/i18n'; import FilterField from './fields/filterField'; import DatasetConfig from './dataSource/datasetConfig'; import CodeExport from './components/codeExport'; import AutoVizPanel from './components/autoViz'; import VisualConfig from './components/visualConfig'; import ExplainData from './components/explainData'; import GeoConfigPanel from './components/leafletRenderer/geoConfigPanel'; import AskViz from './components/askViz'; import { renderSpec } from './store/visualSpecStore'; import FieldsContextWrapper from './fields/fieldsContext'; import { guardDataKeys } from './utils/dataPrep'; import { getComputation } from './computation/clientComputation'; import LogPanel from './fields/datasetFields/logPanel'; import BinPanel from './fields/datasetFields/binPanel'; import RenamePanel from './components/renameField'; import { ErrorContext } from './utils/reportError'; import { ErrorBoundary } from 'react-error-boundary'; import Errorpanel from './components/errorpanel'; import { useCurrentMediaTheme } from './utils/media'; import Painter from './components/painter'; import { classNames, cn, parseErrorMessage } from './utils'; import { VizEmbedMenu } from './components/embedMenu'; import DataBoard from './components/dataBoard'; import SideResize from './components/side-resize'; import { VegaliteMapper } from './lib/vl2gw'; import { newChart } from './models/visSpecHistory'; import ComputedFieldDialog from './components/computedField'; import { VizAppContext } from './store/context'; import { Tabs, TabsList, TabsTrigger } from './components/ui/tabs'; import { ChartPieIcon, CircleStackIcon, ChatBubbleLeftRightIcon } from '@heroicons/react/24/outline'; import { TabsContent } from '@radix-ui/react-tabs'; import { VegaliteChat } from './components/chat'; export type BaseVizProps = IAppI18nProps & IVizProps & IErrorHandlerProps & ISpecProps & IComputationContextProps & { darkMode?: 'light' | 'dark'; }; export const VizApp = observer(function VizApp(props: BaseVizProps) { const { computation, darkMode = 'light', i18nLang = 'en-US', enhanceAPI, i18nResources, themeKey = 'vega', themeConfig, vizThemeConfig, toolbar, geographicData, computationTimeout = 60000, spec, chart, vlSpec, onError, hideSegmentNav, hideProfiling, } = props; const { t, i18n } = useTranslation(); const curLang = i18n.language; useEffect(() => { if (i18nResources) { mergeLocaleRes(i18nResources); } }, [i18nResources]); useEffect(() => { if (i18nLang !== curLang) { setLocaleLanguage(i18nLang); } }, [i18nLang, curLang]); const vizStore = useVizStore(); useEffect(() => { if (geographicData) { vizStore.setGeographicData(geographicData, geographicData.key); } }, [vizStore, geographicData]); useEffect(() => { if (spec) { vizStore.replaceNow(renderSpec(spec, vizStore.meta, vizStore.currentVis.name ?? 'Chart 1', vizStore.currentVis.visId)); } }, [spec, vizStore]); useEffect(() => { if (chart) { vizStore.importCode(chart); } }, [chart, vizStore]); useEffect(() => { if (vlSpec) { const emptyChart = newChart(vizStore.meta, ''); vizStore.replaceNow( VegaliteMapper( vlSpec, [...emptyChart.encodings.dimensions, ...emptyChart.encodings.measures], vizStore.currentVis.visId, vizStore.currentVis.name ?? 'Chart 1' ) ); } }, [vlSpec, vizStore]); const rendererRef = useRef(null); const downloadCSVRef = useRef<{ download: () => void }>({ download() {} }); const reportError = useCallback( (msg: string, code?: number) => { const err = new Error(`Error${code ? `(${code})` : ''}: ${msg}`); console.error(err); onError?.(err); if (code) { vizStore.updateShowErrorResolutionPanel(code, msg); } }, [vizStore, onError] ); const { segmentKey, vizEmbededMenu } = vizStore; const [currentTheme, setCurrentTheme] = useState((vizThemeConfig ?? themeConfig ?? themeKey) as IThemeKey | GWGlobalConfig); const appliedThemeKey = typeof currentTheme === 'string' ? currentTheme : themeKey; const appliedThemeConfig = typeof currentTheme === 'object' ? currentTheme : themeConfig; const wrappedComputation = useMemo( () => (computation ? withErrorReport(withTimeout(computation, computationTimeout), (err) => reportError(parseErrorMessage(err), 501)) : async () => []), [reportError, computation, computationTimeout] ); const [portal, setPortal] = useState(null); return ( Something went wrong} onError={props.onError}>
vizStore.setSegmentKey(v as ISegmentKey)} className="w-full h-full flex flex-col" > {!hideSegmentNav && ( {t('App.segments.data')} {t('App.segments.vis')} {enhanceAPI?.features?.vlChat && ( {t('App.segments.chat')} )} )}
{!props.hideChartNav && (
)}
{enhanceAPI?.features?.askviz && ( )} {vizStore.showDataBoard && } {vizStore.showGeoJSONConfigPanel && }
{ vizEmbededMenu.show && vizStore.closeEmbededMenu(); }} onClick={() => { vizEmbededMenu.show && vizStore.closeEmbededMenu(); }} > {computation && ( )}
{enhanceAPI?.features?.vlChat && ( )}
); }); export function VizAppWithContext(props: IVizAppProps & IComputationProps) { const { computation, onMetaChange, fieldKeyGuard, keepAlive, storeRef, defaultConfig, defaultTab, defaultRenderer, ...rest } = props; // @TODO remove deprecated props const appearance = props.appearance ?? props.dark; const data = props.data ?? props.dataSource; const fields = props.fields ?? props.rawFields ?? []; const { computation: safeComputation, safeMetas, onMetaChange: safeOnMetaChange, } = useMemo(() => { if (data) { if (props.fieldKeyGuard) { const { safeData, safeMetas } = guardDataKeys(data, fields); return { safeMetas, computation: getComputation(safeData), onMetaChange: (safeFID, meta) => { const index = safeMetas.findIndex((x) => x.fid === safeFID); if (index >= 0) { props.onMetaChange?.(fields[index].fid, meta); } }, }; } return { safeMetas: fields, computation: getComputation(data), onMetaChange: props.onMetaChange, }; } return { safeMetas: fields, computation: props.computation, onMetaChange: props.onMetaChange, }; }, [fields, data ? data : props.computation, props.fieldKeyGuard, props.onMetaChange]); const darkMode = useCurrentMediaTheme(appearance); return ( ); }