import { useEffect, useMemo, useRef, useState } from 'react'; import { AgCharts } from 'ag-charts-react'; import type { AgCartesianChartOptions } from 'ag-charts-community'; import { ModuleRegistry } from 'ag-charts-community'; import { AnnotationsModule, ContextMenuModule, CrosshairModule, LicenseManager, ZoomModule, } from 'ag-charts-enterprise'; import { AG_CHARTS_LOCALE_ZH_CN } from 'ag-charts-locale'; LicenseManager.setLicenseKey('[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b'); ModuleRegistry.registerModules([AnnotationsModule, ContextMenuModule, ZoomModule, CrosshairModule]); const API_BASE_URL = 'https://nest.zwgczx.com/api/v1'; const statisticOptions = [ { key: 'minValue', label: '最低值', shortLabel: '低' }, { key: 'maxValue', label: '最高值', shortLabel: '高' }, { key: 'avgValue', label: '平均值', shortLabel: '均' }, { key: 'medianValue', label: '中位数', shortLabel: '中' }, { key: 'dataCount', label: '数据量', shortLabel: '量' }, ] as const; const metricOptions = [ { key: 'cost', label: '造价(元)' }, { key: 'buildingArea', label: '建筑面积指标(元/m²)' }, { key: 'builtArea', label: '建造面积指标(元/m²)' }, { key: 'usableArea', label: '使用面积指标(元/m²)' }, ] as const; const contentOptions = [ { key: 'geoLocation', label: '自然地理区位' }, { key: 'facilityType', label: '设施类别' }, { key: 'constructionStage', label: '建设阶段' }, { key: 'planningForm', label: '规划形式' }, ] as const; const browserTreeDefaults = { treetype: '256', checkStrictly: 'true', requestid: '-1', workflowid: '181028', wfid: '181028', billid: '-1812', isbill: '1', f_weaver_belongto_userid: '267', f_weaver_belongto_usertype: '0', wf_isagent: '0', wf_beagenter: '0', wfTestStr: '', viewtype: '1', fromModule: 'workflow', wfCreater: '267', disabledConditionCache: 'true', companyId: '1', }; const contentTreeConfigs = { geoLocation: { endpoint: '/api/public/browser/data/256', treeid: '94004', fieldid: '305425', defaultExpandedLevel: 1, }, facilityType: { endpoint: '/api/public/browser/data/256', treeid: '94005', fieldid: '305426', defaultExpandedLevel: 3, }, constructionStage: { endpoint: '/api/public/browser/data/256', treeid: '94007', fieldid: '305428', defaultExpandedLevel: 1, }, planningForm: { endpoint: '/api/public/browser/data/256', treeid: '94006', fieldid: '305427', defaultExpandedLevel: 1, }, } as const; const chartLineColors = ['#0078a8', '#d14d72', '#1f8f4d', '#d96f23', '#6b5cc8', '#0d7680', '#9a6b12', '#b24b38']; // const mockGeoLocationPayload = { // checkStrictly: true, // type: 3, // datas: [ // { // allVersionIds: '', // canClick: false, // checkStrictly: true, // dsporder: 0, // icon: '', // id: '95005_22', // isImgIcon: false, // isParent: true, // linkUrl: '/formmode/search/CustomSearchOpenTree.jsp?pid=95005_22', // name: '中国', // pid: '0_0', // selected: false, // title: '中国', // type: '2', // }, // ], // iconSetting: { // bgColor: '#96358a', // icon: 'icon-coms-ModelingEngine', // fontColor: '#fff', // }, // }; type StatisticKey = (typeof statisticOptions)[number]['key']; type MetricKey = (typeof metricOptions)[number]['key']; type ContentKey = (typeof contentOptions)[number]['key']; type GroupKey = 'year'; type ApiBuildingFunctionStat = { group_key?: string | number | null; group_name?: string | null; min_value?: number | null; max_value?: number | null; avg_value?: number | null; median_value?: number | null; data_count?: number | null; }; type ApiBuildingFunctionStatBatchItem = { key?: string; data?: ApiBuildingFunctionStat[]; }; type ChartDatum = { groupName: string; minValue: number | null; maxValue: number | null; avgValue: number | null; medianValue: number | null; dataCount: number | null; }; type TreeNode = { id: string; label: string; children: TreeNode[]; hasChildren: boolean; canClick: boolean; expanded: boolean; loading: boolean; loaded: boolean; }; type SelectedContentNode = { id: string; contentKey: ContentKey; label: string; color: string; }; function formatNumber(value: number, maximumFractionDigits: number) { return value.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits, }); } function formatCostValue(value: number) { const absValue = Math.abs(value); if (absValue >= 10000) { return `${formatNumber(value / 10000, 2)}万元`; } const fractionDigits = absValue > 0 && absValue < 1 ? 4 : 2; return `${formatNumber(value, fractionDigits)}元`; } function formatAreaMetricValue(value: number) { const absValue = Math.abs(value); const fractionDigits = absValue > 0 && absValue < 1 ? 4 : 2; return `${formatNumber(value, fractionDigits)}元/m²`; } function formatChartValue(value: number, metricKey: MetricKey, statisticKey: StatisticKey) { if (statisticKey === 'dataCount') { return formatNumber(value, 0); } if (metricKey === 'cost') { return formatCostValue(value); } return formatAreaMetricValue(value); } function normalizeStat(row: ApiBuildingFunctionStat): ChartDatum { return { groupName: row.group_name || String(row.group_key ?? '未命名'), minValue: row.min_value ?? null, maxValue: row.max_value ?? null, avgValue: row.avg_value ?? null, medianValue: row.median_value ?? null, dataCount: row.data_count ?? null, }; } function buildQuery(params: Record) { const search = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value == null) return; search.set(key, String(value)); }); search.set('currenttime', String(Date.now())); search.set('__random__', String(Date.now())); return search.toString(); } function pickArray(payload: unknown): unknown[] { if (Array.isArray(payload)) return payload; if (!payload || typeof payload !== 'object') return []; const source = payload as Record; const candidates = [ source.datas, source.data, source.data && typeof source.data === 'object' ? (source.data as Record).datas : null, source.data && typeof source.data === 'object' ? (source.data as Record).list : null, source.data && typeof source.data === 'object' ? (source.data as Record).treeDatas : null, source.result, source.rows, source.list, source.treeDatas, source.children, ]; for (const candidate of candidates) { if (Array.isArray(candidate)) return candidate; } return []; } function readText(row: Record, keys: string[]) { for (const key of keys) { const value = row[key]; if (value !== null && value !== undefined && String(value).trim()) { return String(value); } } return ''; } function normalizeTreeRows(rows: unknown[]): TreeNode[] { return rows .filter((row): row is Record => !!row && typeof row === 'object') .map((row, index) => { const children = normalizeTreeRows(pickArray(row.children ?? row.childs ?? row.subs)); const id = readText(row, ['id', 'key', 'value', 'nodeid', 'treeid', 'browserid']) || `node-${index}`; const label = readText(row, ['name', 'label', 'title', 'text', 'showname', 'showName', 'browsername', 'displayName']) || id; const hasChildren = children.length > 0 || row.hasChild === true || row.haschild === true || row.isParent === true || row.isparent === true || row.children === true || row.child === true; const canClick = row.canClick === true || row.canClick === 'true' || row.canclick === true || row.canclick === 'true'; return { id, label, children, hasChildren, canClick, expanded: children.length > 0, loading: false, loaded: children.length > 0, }; }); } function updateNode(nodes: TreeNode[], nodeId: string, updater: (node: TreeNode) => TreeNode): TreeNode[] { return nodes.map((node) => { if (node.id === nodeId) return updater(node); return { ...node, children: updateNode(node.children, nodeId, updater), }; }); } function getSelectionKey(contentKey: ContentKey, nodeId: string) { return `${contentKey}:${nodeId}`; } function getSeriesValueKey(index: number) { return `amount${index}`; } function renderTreeNodes( nodes: TreeNode[], contentKey: ContentKey, selectedNodeKeys: Set, getNodeColor: (contentKey: ContentKey, nodeId: string) => string, onToggle: (nodeId: string) => void, onSelect: (node: TreeNode) => void, depth = 0, ) { return (
    {nodes.map((node) => { const selected = selectedNodeKeys.has(getSelectionKey(contentKey, node.id)); const color = node.canClick ? getNodeColor(contentKey, node.id) : undefined; return (
  • {node.hasChildren ? ( ) : ( )} {node.loading ? 加载中 : null}
    {node.expanded && node.children.length > 0 ? renderTreeNodes(node.children, contentKey, selectedNodeKeys, getNodeColor, onToggle, onSelect, depth + 1) : null}
  • ); })}
); } function App() { const workspaceRef = useRef(null); const chartFrameRef = useRef(null); const treeInitialLoadStartedRef = useRef>({ geoLocation: false, facilityType: false, constructionStage: false, planningForm: false, }); const [statisticKey, setStatisticKey] = useState('avgValue'); const [metricKey, setMetricKey] = useState('cost'); const [groupKey, setGroupKey] = useState('year'); const [statisticMenuOpen, setStatisticMenuOpen] = useState(false); const [metricMenuOpen, setMetricMenuOpen] = useState(false); const [activeContentKey, setActiveContentKey] = useState('geoLocation'); const [treeByContent, setTreeByContent] = useState>({ geoLocation: [], facilityType: [], constructionStage: [], planningForm: [], }); const [treeLoadingByContent, setTreeLoadingByContent] = useState>({ geoLocation: false, facilityType: false, constructionStage: false, planningForm: false, }); const [treeErrorByContent, setTreeErrorByContent] = useState>({ geoLocation: null, facilityType: null, constructionStage: null, planningForm: null, }); const [selectedContentNodes, setSelectedContentNodes] = useState([]); const [chartDataBySelection, setChartDataBySelection] = useState>({}); const [chartQueryVersion, setChartQueryVersion] = useState(0); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(null); const [loadingHint, setLoadingHint] = useState(''); const selectedStatistic = statisticOptions.find((option) => option.key === statisticKey) ?? statisticOptions[0]; const selectedMetric = metricOptions.find((option) => option.key === metricKey) ?? metricOptions[0]; const activeContent = contentOptions.find((option) => option.key === activeContentKey) ?? contentOptions[0]; const activeTree = treeByContent[activeContentKey]; const selectedNodeKeys = useMemo( () => new Set(selectedContentNodes.map((node) => getSelectionKey(node.contentKey, node.id))), [selectedContentNodes], ); const getNodeColor = (contentKey: ContentKey, nodeId: string) => { let hash = 0; const key = getSelectionKey(contentKey, nodeId); for (let index = 0; index < key.length; index += 1) { hash = (hash * 31 + key.charCodeAt(index)) % chartLineColors.length; } return chartLineColors[hash]; }; const fetchContentTree = async (contentKey: ContentKey, nodeId?: string) => { const config = contentTreeConfigs[contentKey]; if (!config) { throw new Error('接口待接入'); } const treeParams = { ...browserTreeDefaults, treeid: config.treeid, cube_treeid: config.treeid, fieldid: config.fieldid, }; const params = nodeId ? { ...treeParams, type: '2', id: nodeId, isVirtual: '', } : { ...treeParams, pageSize: '10', current: '1', min: '1', max: '10', }; const response = await fetch(`${config.endpoint}?${buildQuery(params)}`, { credentials: 'include', headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return normalizeTreeRows(pickArray(await response.json())); }; const loadContentTreeWithDefaultExpansion = async (contentKey: ContentKey) => { const defaultExpandedLevel = contentTreeConfigs[contentKey]?.defaultExpandedLevel ?? 0; const loadChildren = async (nodes: TreeNode[], level: number): Promise => { if (level > defaultExpandedLevel) return nodes; return Promise.all( nodes.map(async (node) => { if (!node.hasChildren) return node; const children = node.loaded ? node.children : await fetchContentTree(contentKey, node.id); return { ...node, children: await loadChildren(children, level + 1), expanded: true, loading: false, loaded: true, hasChildren: node.hasChildren || children.length > 0, }; }), ); }; return loadChildren(await fetchContentTree(contentKey), 1); }; const toggleContentNode = (nodeId: string) => { if (!contentTreeConfigs[activeContentKey]) return; const target = activeTree.find((node) => node.id === nodeId); const visit = (nodes: TreeNode[]): TreeNode | null => { for (const node of nodes) { if (node.id === nodeId) return node; const matched = visit(node.children); if (matched) return matched; } return null; }; const node = target ?? visit(activeTree); if (!node?.hasChildren) return; setTreeByContent((current) => ({ ...current, [activeContentKey]: updateNode(current[activeContentKey], nodeId, (node) => ({ ...node, expanded: node.loaded ? !node.expanded : node.expanded, loading: node.loaded ? node.loading : true, })), })); if (node.loaded) return; const currentContentKey = activeContentKey; fetchContentTree(currentContentKey, nodeId) .then((children) => { setTreeByContent((current) => ({ ...current, [currentContentKey]: updateNode(current[currentContentKey], nodeId, (currentNode) => ({ ...currentNode, children, expanded: true, loading: false, loaded: true, hasChildren: currentNode.hasChildren || children.length > 0, })), })); }) .catch((error) => { setTreeByContent((current) => ({ ...current, [currentContentKey]: updateNode(current[currentContentKey], nodeId, (currentNode) => ({ ...currentNode, loading: false, })), })); setTreeErrorByContent((current) => ({ ...current, [currentContentKey]: error instanceof Error ? error.message : '加载失败', })); }); }; const toggleSelectedContentNode = (node: TreeNode) => { const contentKey = activeContentKey; setSelectedContentNodes((current) => { const selectionKey = getSelectionKey(contentKey, node.id); const existingIndex = current.findIndex((item) => getSelectionKey(item.contentKey, item.id) === selectionKey); if (existingIndex >= 0) { setChartDataBySelection((data) => { const { [selectionKey]: _removed, ...rest } = data; return rest; }); return current.filter((_, index) => index !== existingIndex); } const color = getNodeColor(contentKey, node.id); return [...current, { id: node.id, contentKey, label: node.label, color }]; }); }; const updateMetricKey = (nextMetricKey: MetricKey) => { setMetricKey(nextMetricKey); setChartDataBySelection({}); setLoadError(null); setLoadingHint('正在重新加载数据'); setLoading(true); setChartQueryVersion((version) => version + 1); }; useEffect(() => { if (!contentTreeConfigs[activeContentKey]) return; if (treeByContent[activeContentKey].length > 0 || treeInitialLoadStartedRef.current[activeContentKey]) return; const currentContentKey = activeContentKey; treeInitialLoadStartedRef.current[currentContentKey] = true; setTreeLoadingByContent((current) => ({ ...current, [currentContentKey]: true })); setTreeErrorByContent((current) => ({ ...current, [currentContentKey]: null })); loadContentTreeWithDefaultExpansion(currentContentKey) .then((nodes) => { setTreeByContent((current) => ({ ...current, [currentContentKey]: nodes })); }) .catch((error) => { treeInitialLoadStartedRef.current[currentContentKey] = false; setTreeErrorByContent((current) => ({ ...current, [currentContentKey]: error instanceof Error ? error.message : '加载失败', })); }) .finally(() => { setTreeLoadingByContent((current) => ({ ...current, [currentContentKey]: false })); }); }, [activeContentKey, treeByContent]); useEffect(() => { const controller = new AbortController(); async function loadStats() { if (selectedContentNodes.length === 0) { setChartDataBySelection({}); setLoading(false); setLoadingHint(''); setLoadError(null); return; } setLoading(true); setLoadingHint('正在加载数据'); setLoadError(null); try { const hasAnyMissingNode = selectedContentNodes.some((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]); if (!hasAnyMissingNode) { setLoading(false); setLoadingHint(''); return; } const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostStatsBatch`, { method: 'POST', signal: controller.signal, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ groupBy: groupKey, metric: metricKey, nodes: selectedContentNodes.map((node) => ({ key: getSelectionKey(node.contentKey, node.id), contentKey: node.contentKey, nodeId: node.id, })), }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const payload = (await response.json()) as { data?: ApiBuildingFunctionStatBatchItem[] }; const results = (payload.data ?? []) .filter((item) => item.key) .map((item) => [item.key as string, (item.data ?? []).map(normalizeStat).slice(0, 36)] as const); setChartDataBySelection((current) => ({ ...current, ...Object.fromEntries(results), })); } catch (error) { if (controller.signal.aborted) return; setLoadError(error instanceof Error ? error.message : '接口请求失败'); } finally { if (!controller.signal.aborted) { setLoading(false); setLoadingHint(''); } } } void loadStats(); return () => { controller.abort(); }; }, [chartQueryVersion, groupKey, metricKey, selectedContentNodes]); useEffect(() => { const frame = chartFrameRef.current; const fullscreenTarget = workspaceRef.current; if (!frame || !fullscreenTarget) return; const getFullscreenButton = () => frame.querySelector('.chart-fullscreen-button'); const getStatisticButton = () => frame.querySelector('.ag-charts-myButton-statistic')?.closest('.ag-charts-toolbar__button'); const syncToolbarButtons = () => { const button = getFullscreenButton(); if (button) { let icon = button.querySelector('.ag-charts-myButton-fullScreen'); if (!icon) { button.innerHTML = ''; icon = button.querySelector('.ag-charts-myButton-fullScreen'); } const isFullscreen = document.fullscreenElement === fullscreenTarget; button.classList.toggle('ag-charts-toolbar__button--active', isFullscreen); icon?.classList.toggle('anticon-arrow-salt', !isFullscreen); icon?.classList.toggle('anticon-shrink', isFullscreen); } const statisticButton = getStatisticButton(); if (statisticButton) { statisticButton.classList.add('chart-statistic-button'); statisticButton.setAttribute('aria-expanded', String(statisticMenuOpen)); } }; const toggleFullscreen = () => { if (document.fullscreenElement === fullscreenTarget) { void document.exitFullscreen(); } else { void fullscreenTarget.requestFullscreen(); } }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key !== 'F11') return; event.preventDefault(); event.stopPropagation(); toggleFullscreen(); }; const handleFullscreenChange = () => { syncToolbarButtons(); }; const handleToolbarClick = (event: MouseEvent) => { const target = event.target as Element | null; const button = target?.closest( '.chart-fullscreen-button, .chart-statistic-button', ); if (!button || !frame.contains(button)) return; event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); if (button.classList.contains('chart-statistic-button')) { setMetricMenuOpen(false); setStatisticMenuOpen((open) => !open); } else { toggleFullscreen(); } }; const handleToolbarKeyDown = (event: KeyboardEvent) => { if (event.key !== ' ' && event.key !== 'Enter') return; const target = event.target as Element | null; const button = target?.closest( '.chart-fullscreen-button, .chart-statistic-button', ); if (!button || !frame.contains(button)) return; event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); if (button.classList.contains('chart-statistic-button')) { setMetricMenuOpen(false); setStatisticMenuOpen((open) => !open); } else { toggleFullscreen(); } }; const suppressBrowserContextMenu = (event: MouseEvent) => { event.preventDefault(); }; const observer = new MutationObserver(syncToolbarButtons); document.addEventListener('keydown', handleKeyDown, true); document.addEventListener('fullscreenchange', handleFullscreenChange); document.addEventListener('contextmenu', suppressBrowserContextMenu); frame.addEventListener('contextmenu', suppressBrowserContextMenu); frame.addEventListener('click', handleToolbarClick, true); frame.addEventListener('keydown', handleToolbarKeyDown, true); observer.observe(frame, { childList: true, subtree: true }); syncToolbarButtons(); return () => { document.removeEventListener('keydown', handleKeyDown, true); document.removeEventListener('fullscreenchange', handleFullscreenChange); document.removeEventListener('contextmenu', suppressBrowserContextMenu); frame.removeEventListener('contextmenu', suppressBrowserContextMenu); frame.removeEventListener('click', handleToolbarClick, true); frame.removeEventListener('keydown', handleToolbarKeyDown, true); observer.disconnect(); }; }, [statisticMenuOpen]); const chartOptions = useMemo(() => { const isCount = statisticKey === 'dataCount'; const groupNames: string[] = []; const groupNameSeen = new Set(); selectedContentNodes.forEach((node) => { const rows = chartDataBySelection[getSelectionKey(node.contentKey, node.id)] ?? []; rows.forEach((datum) => { if (groupNameSeen.has(datum.groupName)) return; groupNameSeen.add(datum.groupName); groupNames.push(datum.groupName); }); }); const visibleData = groupNames.map((groupName) => { const row: Record = { groupName }; selectedContentNodes.forEach((node, index) => { const datum = chartDataBySelection[getSelectionKey(node.contentKey, node.id)]?.find((item) => item.groupName === groupName); row[getSeriesValueKey(index)] = datum?.[statisticKey] ?? null; }); return row; }); return { theme: { palette: { fills: ['#006f9b', '#ff7faa', '#00994d', '#ff8833', '#00a0dd'], strokes: ['#003f58', '#934962', '#004a25', '#914d1d', '#006288'], }, params: { foregroundColor: '#262a33', backgroundColor: '#fff1e5', accentColor: '#0d7680', fontFamily: '"Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif', fontSize: 14, tooltipBackgroundColor: '#fff7ef', tooltipTextColor: '#262a33', }, }, locale: { localeText: AG_CHARTS_LOCALE_ZH_CN, }, background: { fill: 'transparent', }, padding: { top: 16, right: 16, bottom: 18, left: 24, }, data: visibleData, zoom: { enabled: true, anchorPointX: 'pointer', anchorPointY: 'pointer', buttons: { enabled: true, visible: 'hover', buttons: [ { icon: 'zoom-out', value: 'zoom-out', section: 'zoom', tooltip: '缩小' }, { icon: 'zoom-in', value: 'zoom-in', section: 'zoom', tooltip: '放大' }, { icon: 'pan-left', value: 'pan-left', section: 'pan', tooltip: '左移' }, { icon: 'pan-right', value: 'pan-right', section: 'pan', tooltip: '右移' }, { icon: 'reset', value: 'reset', section: 'reset', tooltip: '重置' }, ], }, }, contextMenu: { enabled: true, items: ['defaults'], }, overlays: { noData: { text: '请选择右侧分类项', }, }, annotations: { enabled: true, toolbar: { buttons: ([ { value: 'note', tooltip: '切换统计指标', label: `${selectedStatistic.shortLabel}`, }, { icon: 'trend-line-drawing', value: 'line-menu', tooltip: 'Line Tool', }, { icon: 'text-annotation', value: 'text-menu', tooltip: 'Text Tool', }, { icon: 'arrow-drawing', value: 'shape-menu', tooltip: 'Shape Tool', }, { icon: 'fibonacci-retracement-drawing', value: 'fibonacci-menu', tooltip: 'Fibonacci Tool', }, { icon: 'delete', value: 'clear', tooltip: 'Clear annotations', }, ] as unknown as NonNullable['toolbar']>['buttons']), }, }, series: selectedContentNodes.map((node, index) => ({ type: 'line', xKey: 'groupName', yKey: getSeriesValueKey(index), yName: `${node.label} ${selectedStatistic.label}`, stroke: node.color, strokeWidth: 2, marker: { enabled: true, fill: node.color, stroke: node.color, size: 5, }, interpolation: { type: 'smooth', }, tooltip: { renderer: ({ datum, yKey, yName }) => ({ title: yName, data: [ { label: selectedMetric.label, value: formatChartValue(Number(datum[yKey]), metricKey, statisticKey) }, ], }), }, })), axes: { x: { type: 'category', position: 'bottom', line: { enabled: true, stroke: '#c8b9a7', }, tick: { enabled: false, }, label: { color: '#1f2933', fontSize: 12, }, crosshair: { snap: false, }, }, y: { type: 'number', position: 'left', title: { text: '', }, label: { color: '#1f2933', fontSize: 12, formatter: ({ value }) => formatChartValue(Number(value), metricKey, statisticKey), }, line: { enabled: false, }, tick: { enabled: false, }, gridLine: { enabled: true, style: [ { stroke: '#e5d9ca', lineDash: [0], }, ], }, crosshair: { snap: false, }, }, }, legend: { enabled: selectedContentNodes.length > 1, }, tooltip: { enabled: true, }, }; }, [chartDataBySelection, metricKey, selectedContentNodes, selectedMetric.label, selectedStatistic.label, statisticKey]); return (
{statisticMenuOpen ? (
{statisticOptions.map((option) => ( ))}
) : null}
{metricMenuOpen ? (
{metricOptions.map((option) => ( ))}
) : null}
{loading || loadError ?
{loading ? loadingHint || '加载中' : loadError}
: null} {loading ? (
{loadingHint || '加载中'}
) : null}
); } export default App;