- 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.
254 lines
6.1 KiB
JavaScript
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
|