Tom Butcher 3c53278e9f Add ECharts Integration for Enhanced Data Visualization
- Introduced new components, HistoryEChart and HistoryChartLegend, to replace Recharts with ECharts for improved charting capabilities in the dashboard.
- Updated HistoryDisplay and ModelHistoryDisplay components to utilize the new ECharts components, enhancing the overall data visualization experience.
- Implemented a custom tooltip for ECharts to provide detailed information on data points, improving user interaction and data insights.
- Added a hook, useHistoryLegendOverlay, to manage the visibility of the chart legend based on available space, optimizing the layout for different screen sizes.
- Removed unused Recharts imports and related code, streamlining the component structure and improving performance.
2026-08-30 13:54:18 +01:00

254 lines
6.1 KiB
JavaScript

import { useCallback, useEffect, useId, useMemo, useRef } from 'react'
import { Divider, Flex } from 'antd'
import ReactECharts from 'echarts-for-react'
import PropTypes from 'prop-types'
import { useTooltipContext } from '../context/TooltipContext'
const toCssHeight = (height) => {
if (height == null) {
return '100%'
}
if (typeof height === 'number') {
return `${height}px`
}
return /^\d+$/.test(height) ? `${height}px` : height
}
const getTooltipDataIndex = (params) => {
if (params.dataIndex != null) {
return params.dataIndex
}
return (
params.dataByCoordSys?.[0]?.dataByAxis?.[0]?.seriesDataIndices?.[0]
?.dataIndex ?? null
)
}
const HistoryChartTooltipContent = ({ header, items }) => (
<Flex gap={0} vertical style={{ margin: '3px 1px 1px 1px' }}>
<strong>{header}</strong>
<Divider style={{ margin: '3px 0 4px 0' }} />
<table
style={{
borderCollapse: 'collapse',
width: '100%'
}}
>
<tbody>
{items.map(({ label, color, value }) => (
<tr key={label}>
<td
style={{
padding: '1px 6px 1px 0',
verticalAlign: 'middle',
width: 10
}}
>
<span
style={{
display: 'block',
width: 10,
height: 10,
borderRadius: 2,
backgroundColor: color
}}
/>
</td>
<td
style={{
padding: '1px 16px 1px 0',
verticalAlign: 'middle',
whiteSpace: 'nowrap'
}}
>
{label}
</td>
<td
style={{
padding: '1px 0',
verticalAlign: 'middle',
textAlign: 'right',
whiteSpace: 'nowrap'
}}
>
<strong>{value}</strong>
</td>
</tr>
))}
</tbody>
</table>
</Flex>
)
HistoryChartTooltipContent.propTypes = {
header: PropTypes.string.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired
})
).isRequired
}
const HistoryEChart = ({
chartRows,
seriesLabels,
chartType = 'bar',
isDarkMode,
height,
formatTooltipValue
}) => {
const chartRef = useRef(null)
const tooltipId = useId()
const { showTooltip, hideTooltip } = useTooltipContext()
useEffect(() => () => hideTooltip(tooltipId), [hideTooltip, tooltipId])
const handleShowTip = useCallback(
(params) => {
const dataIndex = getTooltipDataIndex(params)
if (dataIndex == null) {
return
}
const row = chartRows[dataIndex]
if (!row) {
return
}
const chart = chartRef.current?.getEchartsInstance()
const dom = chart?.getDom()
if (!dom || params.x == null || params.y == null) {
return
}
const rect = dom.getBoundingClientRect()
const items = seriesLabels.map(({ label, color }) => ({
label,
color,
value: formatTooltipValue
? formatTooltipValue(row[label], label)
: row[label]
}))
showTooltip(
tooltipId,
<HistoryChartTooltipContent header={row.dateFormatted} items={items} />,
rect.left + params.x,
rect.top + params.y
)
},
[chartRows, seriesLabels, formatTooltipValue, showTooltip, tooltipId]
)
const handleHideTip = useCallback(() => {
hideTooltip(tooltipId)
}, [hideTooltip, tooltipId])
const onEvents = useMemo(
() => ({
showTip: handleShowTip,
hideTip: handleHideTip,
globalout: handleHideTip
}),
[handleShowTip, handleHideTip]
)
const option = useMemo(() => {
const axisColor = isDarkMode ? '#d9d9d9' : '#595959'
const gridColor = isDarkMode ? '#303030' : '#f0f0f0'
const categories = chartRows.map((row) => row.dateFormatted)
return {
textStyle: {
fontFamily: "'DM Sans', sans-serif"
},
grid: {
left: 8,
right: 8,
top: 8,
bottom: 8,
containLabel: true
},
tooltip: {
trigger: 'axis',
showContent: false,
axisPointer: {
type: chartType === 'line' ? 'line' : 'shadow'
}
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
color: axisColor,
fontSize: 12
},
axisLine: {
lineStyle: { color: gridColor }
},
axisTick: {
lineStyle: { color: gridColor }
}
},
yAxis: {
type: 'value',
axisLabel: {
color: axisColor,
fontSize: 12
},
splitLine: {
lineStyle: {
color: gridColor,
type: 'dashed'
}
}
},
series: seriesLabels.map(({ label, color }) => ({
name: label,
type: chartType,
stack: chartType === 'bar' ? 'history' : undefined,
smooth: chartType === 'line',
showSymbol: false,
emphasis: { focus: 'series' },
itemStyle: { color },
lineStyle: chartType === 'line' ? { width: 2, color } : undefined,
data: chartRows.map((row) => row[label] ?? 0)
}))
}
}, [chartRows, seriesLabels, chartType, isDarkMode])
return (
<ReactECharts
ref={chartRef}
option={option}
style={{ width: '100%', height: toCssHeight(height) }}
onEvents={onEvents}
notMerge
lazyUpdate
/>
)
}
HistoryEChart.propTypes = {
chartRows: PropTypes.arrayOf(PropTypes.object).isRequired,
seriesLabels: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
color: PropTypes.string.isRequired
})
).isRequired,
chartType: PropTypes.oneOf(['line', 'bar']),
isDarkMode: PropTypes.bool,
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
formatTooltipValue: PropTypes.func
}
export default HistoryEChart