ComposedChart.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. import { useTheme } from 'next-themes'
  2. import { ComponentProps, useEffect, useMemo, useState } from 'react'
  3. import {
  4. Area,
  5. Bar,
  6. CartesianGrid,
  7. Customized,
  8. Label,
  9. Line,
  10. ComposedChart as RechartComposedChart,
  11. ReferenceArea,
  12. ReferenceLine,
  13. Tooltip,
  14. XAxis,
  15. YAxis,
  16. } from 'recharts'
  17. import { CategoricalChartState } from 'recharts/types/chart/types'
  18. import { cn } from 'ui'
  19. import { ChartHeader } from './ChartHeader'
  20. import { ChartHighlightAction, ChartHighlightActions } from './ChartHighlightActions'
  21. import {
  22. CHART_COLORS,
  23. DateTimeFormats,
  24. STACKED_CHART_COLORS,
  25. STACKED_CHART_FILLS,
  26. updateStackedChartColors,
  27. } from './Charts.constants'
  28. import { CommonChartProps, Datum } from './Charts.types'
  29. import {
  30. computeYAxisDomain,
  31. formatPercentage,
  32. normalizeStackedSeriesData,
  33. numberFormatter,
  34. useChartSize,
  35. } from './Charts.utils'
  36. import {
  37. calculateTotalChartAggregate,
  38. CustomLabel,
  39. CustomTooltip,
  40. MultiAttribute,
  41. } from './ComposedChart.utils'
  42. import NoDataPlaceholder from './NoDataPlaceholder'
  43. import { ChartHighlight } from './useChartHighlight'
  44. import { useChartHoverState } from './useChartHoverState'
  45. import { formatDateTime, useFormatDateTime } from '@/lib/datetime'
  46. import { formatBytes, formatBytesMinMB } from '@/lib/helpers'
  47. export interface ComposedChartProps<D = Datum> extends CommonChartProps<D> {
  48. chartId?: string
  49. attributes: MultiAttribute[]
  50. yAxisKey: string
  51. xAxisKey: string
  52. displayDateInUtc?: boolean
  53. onBarClick?: (datum: Datum, tooltipData?: CategoricalChartState) => void
  54. emptyStateMessage?: string
  55. showLegend?: boolean
  56. xAxisIsDate?: boolean
  57. XAxisProps?: ComponentProps<typeof XAxis>
  58. YAxisProps?: ComponentProps<typeof YAxis>
  59. showGrid?: boolean
  60. showTooltip?: boolean
  61. showTotal?: boolean
  62. showMaxValue?: boolean
  63. chartHighlight?: ChartHighlight
  64. hideChartType?: boolean
  65. chartStyle?: string
  66. onChartStyleChange?: (style: string) => void
  67. updateDateRange: any
  68. titleTooltip?: string
  69. hideYAxis?: boolean
  70. hideHighlightedValue?: boolean
  71. hideHighlightedLabel?: boolean
  72. hideHighlightArea?: boolean
  73. syncId?: string
  74. docsUrl?: string
  75. sql?: string
  76. highlightActions?: ChartHighlightAction[]
  77. showNewBadge?: boolean
  78. normalizeVisibleStackToPercent?: boolean
  79. }
  80. interface CustomizedDotProps {
  81. formattedGraphicalItems?: Array<{
  82. props?: {
  83. points?: Array<{ x: number; y: number }>
  84. dataKey?: string
  85. }
  86. item?: {
  87. props?: {
  88. points?: Array<{ x: number; y: number }>
  89. dataKey?: string
  90. }
  91. }
  92. points?: Array<{ x: number; y: number }>
  93. }>
  94. }
  95. export function ComposedChart({
  96. chartId,
  97. data,
  98. attributes,
  99. yAxisKey,
  100. xAxisKey,
  101. format,
  102. customDateFormat = DateTimeFormats.FULL,
  103. title,
  104. highlightedValue,
  105. highlightedLabel,
  106. displayDateInUtc,
  107. minimalHeader,
  108. valuePrecision,
  109. className = '',
  110. size = 'normal',
  111. emptyStateMessage,
  112. onBarClick,
  113. showLegend = false,
  114. xAxisIsDate = true,
  115. XAxisProps,
  116. YAxisProps,
  117. showGrid = false,
  118. showTooltip = false,
  119. showTotal = true,
  120. showMaxValue = false,
  121. chartHighlight,
  122. hideChartType,
  123. chartStyle,
  124. onChartStyleChange,
  125. updateDateRange,
  126. hideYAxis,
  127. hideHighlightedValue,
  128. hideHighlightedLabel = false,
  129. hideHighlightArea = false,
  130. syncId,
  131. docsUrl,
  132. sql,
  133. highlightActions,
  134. titleTooltip,
  135. showNewBadge,
  136. normalizeVisibleStackToPercent = false,
  137. }: ComposedChartProps) {
  138. const { resolvedTheme } = useTheme()
  139. const { hoveredIndex, syncTooltip, setHover, clearHover } = useChartHoverState(
  140. syncId || 'default'
  141. )
  142. const [_showMaxValue, setShowMaxValue] = useState(showMaxValue)
  143. const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
  144. const [isActiveHoveredChart, setIsActiveHoveredChart] = useState(false)
  145. const [hiddenAttributes, setHiddenAttributes] = useState<Set<string>>(new Set())
  146. const isDarkMode = resolvedTheme?.includes('dark')
  147. useEffect(() => {
  148. updateStackedChartColors(isDarkMode ?? false)
  149. }, [isDarkMode])
  150. const { Container } = useChartSize(size)
  151. // When `displayDateInUtc` is set the chart explicitly wants UTC labels.
  152. // Otherwise honour the user's selected timezone via the picker.
  153. const formatPickerDate = useFormatDateTime()
  154. const formatChartDate = (value: number | string) =>
  155. displayDateInUtc
  156. ? formatDateTime(value, { tz: 'UTC', format: customDateFormat })
  157. : formatPickerDate(value, customDateFormat)
  158. const formatTimestamp = (ts: unknown) => {
  159. if (typeof ts !== 'number' && typeof ts !== 'string') {
  160. return ''
  161. }
  162. if (typeof ts === 'number' && ts > 1e14) {
  163. // Microsecond timestamp; convert to milliseconds before formatting.
  164. return formatChartDate(ts / 1000)
  165. }
  166. return formatChartDate(ts)
  167. }
  168. const _XAxisProps = XAxisProps || {
  169. interval: data.length - 2,
  170. angle: 0,
  171. tick: false,
  172. }
  173. const _YAxisProps = YAxisProps || {
  174. tickFormatter: (value) => numberFormatter(value, valuePrecision),
  175. tick: false,
  176. width: 0,
  177. }
  178. const yAxisPadding = useMemo(() => {
  179. const needsTopPadding = normalizeVisibleStackToPercent && chartStyle !== 'bar'
  180. if (!needsTopPadding) return _YAxisProps.padding
  181. return {
  182. ..._YAxisProps.padding,
  183. top: Math.max(8, _YAxisProps.padding?.top ?? 0),
  184. }
  185. }, [_YAxisProps.padding, chartStyle, normalizeVisibleStackToPercent])
  186. function getHeaderLabel() {
  187. if (!xAxisIsDate) {
  188. if (!focusDataIndex) return highlightedLabel
  189. return data[focusDataIndex]?.[xAxisKey]
  190. }
  191. return (
  192. (focusDataIndex !== null &&
  193. data &&
  194. data[focusDataIndex] !== undefined &&
  195. (() => {
  196. const ts = data[focusDataIndex][xAxisKey]
  197. return formatTimestamp(ts)
  198. })()) ||
  199. highlightedLabel
  200. )
  201. }
  202. function formatHighlightedValue(value: any) {
  203. if (typeof value !== 'number') {
  204. return value
  205. }
  206. if (shouldFormatBytes) {
  207. const bytesValue = isNetworkChart ? Math.abs(value) : value
  208. const formatted = isMemoryChart
  209. ? formatBytesMinMB(bytesValue, valuePrecision)
  210. : formatBytes(bytesValue, valuePrecision)
  211. return format === 'bytes-per-second' ? `${formatted}/s` : formatted
  212. }
  213. if (format === '%') {
  214. return formatPercentage(value, valuePrecision)
  215. }
  216. if (valuePrecision === 0 && value > 0 && value < 1) {
  217. return '<1'
  218. }
  219. const formatted = numberFormatter(value, valuePrecision)
  220. if (typeof format === 'string' && format) {
  221. return `${formatted}${format}`
  222. }
  223. return formatted
  224. }
  225. function computeHighlightedValue() {
  226. const referenceLines = attributes.filter(
  227. (attribute) => attribute?.provider === 'reference-line'
  228. )
  229. const attributesToIgnore =
  230. attributes?.filter((a) => a.omitFromTotal)?.map((a) => a.attribute) ?? []
  231. const attributesToIgnoreFromTotal = [
  232. ...attributesToIgnore,
  233. ...(referenceLines?.map((a: MultiAttribute) => a.attribute) ?? []),
  234. ...(maxAttribute?.attribute ? [maxAttribute?.attribute] : []),
  235. ...Array.from(hiddenAttributes),
  236. ]
  237. const lastDataPoint = data[data.length - 1]
  238. ? Object.entries(data[data.length - 1])
  239. .map(([key, value]) => ({
  240. dataKey: key,
  241. value: value as number,
  242. }))
  243. .filter(
  244. (entry) =>
  245. entry.dataKey !== 'timestamp' &&
  246. entry.dataKey !== 'period_start' &&
  247. attributes.some((attr) => attr.attribute === entry.dataKey && attr.enabled !== false)
  248. )
  249. : undefined
  250. if (focusDataIndex !== null) {
  251. const focusedDataPoint = data[focusDataIndex]
  252. ? Object.entries(data[focusDataIndex])
  253. .map(([key, value]) => ({
  254. dataKey: key,
  255. value: value as number,
  256. }))
  257. .filter(
  258. (entry) =>
  259. entry.dataKey !== 'timestamp' &&
  260. entry.dataKey !== 'period_start' &&
  261. attributes.some(
  262. (attr) => attr.attribute === entry.dataKey && attr.enabled !== false
  263. )
  264. )
  265. : undefined
  266. return showTotal
  267. ? calculateTotalChartAggregate(focusedDataPoint ?? [], attributesToIgnoreFromTotal)
  268. : data[focusDataIndex]?.[yAxisKey]
  269. }
  270. if (showTotal && lastDataPoint) {
  271. return calculateTotalChartAggregate(lastDataPoint, attributesToIgnoreFromTotal)
  272. }
  273. return highlightedValue
  274. }
  275. const maxAttribute = attributes.find((a) => a.isMaxValue)
  276. const maxAttributeData = {
  277. name: maxAttribute?.attribute,
  278. color: CHART_COLORS.REFERENCE_LINE,
  279. }
  280. const referenceLines = attributes.filter((attribute) => {
  281. return attribute?.provider === 'reference-line'
  282. })
  283. const resolvedHighlightedLabel = getHeaderLabel()
  284. const resolvedHighlightedValue = computeHighlightedValue()
  285. const showHighlightActions =
  286. chartHighlight?.coordinates.left &&
  287. chartHighlight?.coordinates.right &&
  288. chartHighlight?.coordinates.left !== chartHighlight?.coordinates.right
  289. const chartData =
  290. data && !!data[0]
  291. ? Object.entries(data[0])
  292. ?.map(([key, value]) => ({
  293. name: key,
  294. value: value,
  295. }))
  296. .filter(
  297. (att) =>
  298. att.name !== 'timestamp' &&
  299. att.name !== 'period_start' &&
  300. att.name !== maxAttribute?.attribute &&
  301. !referenceLines.map((a) => a.attribute).includes(att.name) &&
  302. attributes.some((attr) => attr.attribute === att.name && attr.enabled !== false)
  303. )
  304. .map((att, index) => {
  305. const attribute = attributes.find((attr) => attr.attribute === att.name)
  306. return {
  307. ...att,
  308. color: attribute?.color
  309. ? isDarkMode
  310. ? attribute.color.dark
  311. : attribute.color.light
  312. : STACKED_CHART_COLORS[index % STACKED_CHART_COLORS.length],
  313. fill: attribute?.fill
  314. ? isDarkMode
  315. ? attribute.fill.dark
  316. : attribute.fill.light
  317. : STACKED_CHART_FILLS[index % STACKED_CHART_FILLS.length],
  318. }
  319. })
  320. : []
  321. const stackedAttributes = chartData.filter((att) => {
  322. const attribute = attributes.find((attr) => attr.attribute === att.name)
  323. return !attribute?.isMaxValue
  324. })
  325. const visibleAttributes = useMemo(
  326. () => stackedAttributes.filter((att) => !hiddenAttributes.has(att.name)),
  327. [stackedAttributes, hiddenAttributes]
  328. )
  329. const displayData = useMemo(
  330. () =>
  331. normalizeVisibleStackToPercent
  332. ? normalizeStackedSeriesData({
  333. data,
  334. attributeNames: visibleAttributes.map((attribute) => attribute.name),
  335. })
  336. : data,
  337. [data, normalizeVisibleStackToPercent, visibleAttributes]
  338. )
  339. const isPercentage = format === '%'
  340. const isRamChart =
  341. !chartData?.some((att: any) => att.name.toLowerCase() === 'ram_usage') &&
  342. chartData?.some((att: any) => att.name.toLowerCase().includes('ram_'))
  343. const isSwapChart = chartData?.some((att: any) => att.name.toLowerCase().includes('swap_'))
  344. const isMemoryChart = isRamChart || isSwapChart
  345. const isDiskSpaceChart = chartData?.some((att: any) =>
  346. att.name.toLowerCase().includes('disk_space_')
  347. )
  348. const isDBSizeChart = chartData?.some((att: any) =>
  349. att.name.toLowerCase().includes('pg_database_size')
  350. )
  351. const isNetworkChart = chartData?.some((att: any) => att.name.toLowerCase().includes('network_'))
  352. const isBytesFormat = format === 'bytes' || format === 'bytes-per-second'
  353. const shouldFormatBytes =
  354. isBytesFormat || isMemoryChart || isDiskSpaceChart || isDBSizeChart || isNetworkChart
  355. const yMaxFromVisible = Math.max(
  356. 0,
  357. ...visibleAttributes.map((att) => (typeof att.value === 'number' ? att.value : 0))
  358. )
  359. const yAxisDomain = useMemo(
  360. () =>
  361. computeYAxisDomain({
  362. isPercentage,
  363. showMaxValue,
  364. yMaxFromVisible,
  365. maxAttributeKey: maxAttribute?.attribute,
  366. showMaxLine: _showMaxValue,
  367. data,
  368. visibleAttributeNames: visibleAttributes.map((a) => a.name),
  369. }),
  370. [
  371. isPercentage,
  372. showMaxValue,
  373. yMaxFromVisible,
  374. maxAttribute,
  375. _showMaxValue,
  376. data,
  377. visibleAttributes,
  378. ]
  379. )
  380. if (data.length === 0) {
  381. return (
  382. <NoDataPlaceholder
  383. hideTotalPlaceholder={highlightedValue === undefined}
  384. message={emptyStateMessage}
  385. description="It may take up to 24 hours for data to refresh"
  386. size={size}
  387. className={className}
  388. attribute={title}
  389. format={format}
  390. titleTooltip={titleTooltip}
  391. />
  392. )
  393. }
  394. return (
  395. <div className={cn('flex flex-col gap-y-3', className)}>
  396. <ChartHeader
  397. hideHighlightedValue={hideHighlightedValue}
  398. title={title}
  399. showNewBadge={showNewBadge}
  400. format={format}
  401. hideHighlightedLabel={hideHighlightedLabel}
  402. hideHighlightArea={hideHighlightArea}
  403. titleTooltip={titleTooltip}
  404. customDateFormat={customDateFormat}
  405. highlightedValue={formatHighlightedValue(resolvedHighlightedValue)}
  406. highlightedLabel={resolvedHighlightedLabel}
  407. minimalHeader={minimalHeader}
  408. hideChartType={hideChartType}
  409. chartStyle={chartStyle}
  410. onChartStyleChange={onChartStyleChange}
  411. showMaxValue={_showMaxValue}
  412. setShowMaxValue={maxAttribute ? setShowMaxValue : undefined}
  413. docsUrl={docsUrl}
  414. syncId={syncId}
  415. data={data}
  416. xAxisKey={xAxisKey}
  417. yAxisKey={yAxisKey}
  418. xAxisIsDate={xAxisIsDate}
  419. displayDateInUtc={displayDateInUtc}
  420. valuePrecision={valuePrecision}
  421. shouldFormatBytes={shouldFormatBytes}
  422. isNetworkChart={isNetworkChart}
  423. isMemoryChart={isMemoryChart}
  424. attributes={attributes}
  425. sql={sql}
  426. />
  427. <Container className="relative z-10">
  428. <RechartComposedChart
  429. data={displayData}
  430. syncId={syncId}
  431. style={{ cursor: 'crosshair' }}
  432. onMouseMove={({ activeLabel, activeTooltipIndex }) => {
  433. if (activeTooltipIndex === undefined || activeTooltipIndex === null) return
  434. setIsActiveHoveredChart(true)
  435. if (activeTooltipIndex !== focusDataIndex) {
  436. setFocusDataIndex(activeTooltipIndex)
  437. }
  438. setHover(activeTooltipIndex)
  439. const activeTimestamp =
  440. data[activeTooltipIndex]?.[xAxisKey] ?? data[activeTooltipIndex]?.timestamp
  441. chartHighlight?.handleMouseMove({
  442. activeLabel: activeTimestamp?.toString(),
  443. coordinates: activeLabel,
  444. })
  445. }}
  446. onMouseDown={({ activeLabel, activeTooltipIndex }) => {
  447. if (activeTooltipIndex === undefined || activeTooltipIndex === null) return
  448. const activeTimestamp =
  449. data[activeTooltipIndex]?.[xAxisKey] ?? data[activeTooltipIndex]?.timestamp
  450. chartHighlight?.handleMouseDown({
  451. activeLabel: activeTimestamp?.toString(),
  452. coordinates: activeLabel,
  453. })
  454. }}
  455. onMouseUp={chartHighlight?.handleMouseUp}
  456. onMouseLeave={() => {
  457. setIsActiveHoveredChart(false)
  458. setFocusDataIndex(null)
  459. clearHover()
  460. }}
  461. onClick={(tooltipData) => {
  462. const datum = tooltipData?.activePayload?.[0]?.payload
  463. if (onBarClick) onBarClick(datum, tooltipData)
  464. }}
  465. >
  466. {showGrid && <CartesianGrid stroke={CHART_COLORS.AXIS} />}
  467. <YAxis
  468. {..._YAxisProps}
  469. hide={hideYAxis}
  470. axisLine={{ stroke: CHART_COLORS.AXIS }}
  471. tickLine={{ stroke: CHART_COLORS.AXIS }}
  472. domain={_YAxisProps.domain ?? yAxisDomain}
  473. padding={yAxisPadding}
  474. key={yAxisKey}
  475. />
  476. <XAxis
  477. {..._XAxisProps}
  478. axisLine={{ stroke: CHART_COLORS.AXIS }}
  479. tickLine={{ stroke: CHART_COLORS.AXIS }}
  480. tickMargin={8}
  481. minTickGap={3}
  482. key={xAxisKey}
  483. />
  484. <defs>
  485. {visibleAttributes.map((attribute) => (
  486. <linearGradient
  487. key={`gradient-${attribute.name}`}
  488. id={`gradient-${attribute.name}`}
  489. x1="0"
  490. y1="0"
  491. x2="0"
  492. y2="1"
  493. >
  494. <stop offset="5%" stopColor={attribute.color} stopOpacity={0.15} />
  495. <stop offset="95%" stopColor={isDarkMode ? '#131313' : '#FFFFFF'} stopOpacity={0} />
  496. </linearGradient>
  497. ))}
  498. </defs>
  499. {chartStyle === 'bar'
  500. ? visibleAttributes.map((attribute) => (
  501. <Bar
  502. key={attribute.name}
  503. dataKey={attribute.name}
  504. stackId={attributes?.find((a) => a.attribute === attribute?.name)?.stackId ?? '1'}
  505. fill={attribute.color}
  506. radius={0.75}
  507. opacity={1}
  508. name={
  509. attributes?.find((a) => a.attribute === attribute?.name)?.label ||
  510. attribute?.name
  511. }
  512. maxBarSize={24}
  513. />
  514. ))
  515. : visibleAttributes.map((attribute) => (
  516. <Area
  517. key={attribute.name}
  518. type="linear"
  519. dataKey={attribute.name}
  520. stackId="1"
  521. fill={`url(#gradient-${attribute.name})`}
  522. fillOpacity={1}
  523. stroke={attribute.color}
  524. radius={20}
  525. animationDuration={375}
  526. name={
  527. attributes?.find((a) => a.attribute === attribute.name)?.label || attribute.name
  528. }
  529. dot={false}
  530. activeDot={false}
  531. />
  532. ))}
  533. {/* Max value, if available */}
  534. {maxAttribute && _showMaxValue && (
  535. <Line
  536. key={maxAttribute.attribute}
  537. type="linear"
  538. dataKey={maxAttribute.attribute}
  539. stroke={CHART_COLORS.REFERENCE_LINE}
  540. strokeWidth={2}
  541. strokeDasharray={maxAttribute.strokeDasharray ?? '3 3'}
  542. dot={false}
  543. name={maxAttribute.label}
  544. />
  545. )}
  546. {referenceLines
  547. .filter((line) => {
  548. return line.isReferenceLine
  549. })
  550. .map((line) => (
  551. <ReferenceLine
  552. key={line.attribute}
  553. y={line.value}
  554. strokeWidth={1}
  555. stroke={isDarkMode ? line.color?.dark : line.color?.light}
  556. strokeDasharray={line.strokeDasharray ?? '3 3'}
  557. label={undefined}
  558. >
  559. <Label
  560. value={line.label}
  561. position="insideTopRight"
  562. fill={CHART_COLORS.REFERENCE_LINE_TEXT}
  563. className="text-xs"
  564. style={{ fill: CHART_COLORS.REFERENCE_LINE_TEXT }}
  565. />
  566. </ReferenceLine>
  567. ))}
  568. {/* Selection highlight */}
  569. {showHighlightActions && (
  570. <ReferenceArea
  571. x1={chartHighlight?.coordinates.left}
  572. x2={chartHighlight?.coordinates.right}
  573. strokeOpacity={0.5}
  574. stroke={isDarkMode ? '#FFFFFF' : '#0C3925'}
  575. fill={isDarkMode ? '#FFFFFF' : '#0C3925'}
  576. fillOpacity={0.2}
  577. />
  578. )}
  579. <Tooltip
  580. content={(props) =>
  581. showTooltip && !showHighlightActions ? (
  582. <CustomTooltip
  583. {...props}
  584. data={data}
  585. format={format}
  586. isPercentage={isPercentage}
  587. label={resolvedHighlightedLabel}
  588. attributes={attributes}
  589. xAxisKey={xAxisKey}
  590. valuePrecision={valuePrecision}
  591. showTotal={showTotal}
  592. isActiveHoveredChart={
  593. isActiveHoveredChart || (!!syncId && syncTooltip && hoveredIndex !== null)
  594. }
  595. />
  596. ) : null
  597. }
  598. cursor={{
  599. stroke: isDarkMode ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)',
  600. strokeWidth: 1,
  601. }}
  602. />
  603. <Customized
  604. component={(props: CustomizedDotProps) => {
  605. const { formattedGraphicalItems } = props
  606. if (!formattedGraphicalItems || focusDataIndex === null) return null
  607. return (
  608. <g>
  609. {formattedGraphicalItems.map((item, index: number) => {
  610. const points = item.props?.points || item.item?.props?.points || item.points
  611. const dataKey = item.props?.dataKey || item.item?.props?.dataKey
  612. if (!points || !points[focusDataIndex]) return null
  613. const point = points[focusDataIndex]
  614. const attribute = visibleAttributes.find((a) => a.name === dataKey)
  615. if (!attribute) return null
  616. return (
  617. <circle
  618. key={`custom-dot-${dataKey}-${index}`}
  619. cx={point.x}
  620. cy={point.y}
  621. r={4}
  622. fill={attribute.fill}
  623. stroke={attribute.color}
  624. strokeWidth={1}
  625. />
  626. )
  627. })}
  628. </g>
  629. )
  630. }}
  631. />
  632. </RechartComposedChart>
  633. </Container>
  634. <ChartHighlightActions
  635. chartHighlight={chartHighlight}
  636. updateDateRange={updateDateRange}
  637. actions={highlightActions}
  638. chartId={chartId}
  639. />
  640. {data && (
  641. <div
  642. className="text-foreground-lighter -mt-9 flex items-center justify-between text-xs"
  643. style={{ marginLeft: YAxisProps?.width }}
  644. >
  645. <span>{xAxisIsDate ? formatTimestamp(data[0]?.[xAxisKey]) : data[0]?.[xAxisKey]}</span>
  646. <span>
  647. {xAxisIsDate
  648. ? formatTimestamp(data[data.length - 1]?.[xAxisKey])
  649. : data[data.length - 1]?.[xAxisKey]}
  650. </span>
  651. </div>
  652. )}
  653. {showLegend && (
  654. <div className="relative z-0">
  655. <CustomLabel
  656. payload={[maxAttributeData, ...chartData]}
  657. attributes={attributes}
  658. showMaxValue={_showMaxValue}
  659. onToggleAttribute={(attribute, options) => {
  660. setHiddenAttributes((prev) => {
  661. if (options?.exclusive) {
  662. // Hide every attribute except the selected one. If all but one are hidden, clicking again will reset to all visible.
  663. const allNames = chartData.map((c) => c.name)
  664. const allHiddenExcept = allNames.filter((n) => n !== attribute)
  665. const isAlreadyExclusive =
  666. allHiddenExcept.every((n) => prev.has(n)) && !prev.has(attribute)
  667. return isAlreadyExclusive ? new Set() : new Set(allHiddenExcept)
  668. }
  669. const next = new Set(prev)
  670. if (next.has(attribute)) {
  671. next.delete(attribute)
  672. } else {
  673. next.add(attribute)
  674. }
  675. return next
  676. })
  677. }}
  678. hiddenAttributes={hiddenAttributes}
  679. />
  680. </div>
  681. )}
  682. </div>
  683. )
  684. }