| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- import { useState } from 'react'
- import { Bar, BarChart, Cell, Legend, Tooltip, XAxis } from 'recharts'
- import { ChartHeader } from './ChartHeader'
- import {
- CHART_COLORS,
- DateTimeFormats,
- DEFAULT_STACK_COLORS,
- genStackColorScales,
- ValidStackColor,
- } from './Charts.constants'
- import type { CommonChartProps } from './Charts.types'
- import { numberFormatter, precisionFormatter, useChartSize, useStacked } from './Charts.utils'
- import NoDataPlaceholder from './NoDataPlaceholder'
- import { useChartHoverState } from './useChartHoverState'
- import { formatDateTime, useFormatDateTime } from '@/lib/datetime'
- interface Props extends CommonChartProps<any> {
- xAxisKey: string
- yAxisKey: string
- stackKey: string
- onBarClick?: () => void
- variant?: 'values' | 'percentages'
- xAxisFormatAsDate?: boolean
- displayDateInUtc?: boolean
- hideLegend?: boolean
- hideHeader?: boolean
- stackColors?: ValidStackColor[]
- syncId?: string
- }
- const StackedBarChart: React.FC<Props> = ({
- size,
- data,
- xAxisKey,
- stackKey,
- yAxisKey,
- customDateFormat = DateTimeFormats.FULL,
- title,
- highlightedValue,
- highlightedLabel,
- format,
- minimalHeader = false,
- valuePrecision,
- onBarClick,
- variant,
- xAxisFormatAsDate = true,
- displayDateInUtc,
- hideLegend = false,
- hideHeader = false,
- stackColors = DEFAULT_STACK_COLORS,
- syncId,
- }) => {
- const { Container } = useChartSize(size)
- const { hoveredIndex, syncTooltip, setHover, clearHover } = useChartHoverState(
- syncId || 'default'
- )
- const { dataKeys, stackedData, percentagesStackedData } = useStacked({
- data,
- xAxisKey,
- stackKey,
- yAxisKey,
- variant,
- })
- const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
- // When `displayDateInUtc` is set the chart explicitly wants UTC labels.
- // Otherwise honour the user's selected timezone via the picker.
- const formatPickerDate = useFormatDateTime()
- const formatChartDate = (value: number | string) =>
- displayDateInUtc
- ? formatDateTime(value, { tz: 'UTC', format: customDateFormat })
- : formatPickerDate(value, customDateFormat)
- const resolvedHighlightedLabel =
- (focusDataIndex !== null &&
- data &&
- data[focusDataIndex] !== undefined &&
- formatChartDate(data[focusDataIndex][xAxisKey])) ||
- highlightedLabel
- const resolvedHighlightedValue =
- focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue
- if (!data || data.length === 0) {
- return (
- <NoDataPlaceholder
- description="It may take up to 24 hours for data to refresh"
- size={size}
- attribute={title}
- format={format}
- />
- )
- }
- const stackColorScales = genStackColorScales(stackColors)
- return (
- <div className="w-full">
- {!hideHeader && (
- <ChartHeader
- title={title}
- format={format}
- customDateFormat={customDateFormat}
- minimalHeader={minimalHeader}
- highlightedValue={
- typeof resolvedHighlightedValue === 'number'
- ? numberFormatter(resolvedHighlightedValue, valuePrecision)
- : resolvedHighlightedValue
- }
- highlightedLabel={resolvedHighlightedLabel}
- syncId={syncId}
- data={data}
- xAxisKey={xAxisKey}
- yAxisKey={yAxisKey}
- xAxisIsDate={xAxisFormatAsDate}
- displayDateInUtc={displayDateInUtc}
- valuePrecision={valuePrecision}
- attributes={[]}
- />
- )}
- <Container>
- <BarChart
- data={variant === 'percentages' ? percentagesStackedData : stackedData}
- margin={{
- top: 20,
- right: 20,
- left: 20,
- bottom: 5,
- }}
- className="cursor-pointer overflow-visible"
- // mouse hover focusing logic
- onMouseMove={(e: any) => {
- if (e.activeTooltipIndex !== focusDataIndex) {
- setFocusDataIndex(e.activeTooltipIndex)
- }
- setHover(e.activeTooltipIndex)
- }}
- onMouseLeave={() => {
- setFocusDataIndex(null)
- clearHover()
- }}
- >
- {!hideLegend && (
- <Legend
- wrapperStyle={{ top: -6, fontSize: '0.8rem' }}
- iconSize={8}
- iconType="circle"
- verticalAlign="top"
- />
- )}
- <XAxis
- dataKey={xAxisKey}
- interval={data.length - 2}
- angle={0}
- tick={false}
- axisLine={{ stroke: CHART_COLORS.AXIS }}
- tickLine={{ stroke: CHART_COLORS.AXIS }}
- />
- {dataKeys.map((datum, stackIndex) => (
- <Bar
- key={stackIndex}
- dataKey={datum}
- type="monotone"
- legendType="circle"
- fill={stackColorScales[stackIndex].base}
- stackId={1}
- animationDuration={300}
- maxBarSize={48}
- className={onBarClick ? 'cursor-pointer' : ''}
- >
- {stackedData?.map((_entry: unknown, index: any) => (
- <Cell
- key={`cell-${index}`}
- className={`transition-all duration-300`}
- opacity={focusDataIndex === index ? 0.85 : 1}
- />
- ))}
- </Bar>
- ))}
- <Tooltip
- labelFormatter={
- xAxisFormatAsDate ? (label) => formatChartDate(label as number | string) : undefined
- }
- formatter={(value, name, props) => {
- const suffix = format || ''
- if (variant === 'percentages' && percentagesStackedData) {
- const index = percentagesStackedData.findIndex(
- (pStack) => pStack === props.payload!
- )
- const val = stackedData[index][name]
- const percentage = precisionFormatter(Number(value) * 100, 1) + '%'
- return `${percentage} (${val}${suffix})`
- }
- return String(value) + suffix
- }}
- cursor={false}
- labelClassName="text-white"
- contentStyle={{
- backgroundColor: '#444444',
- borderColor: '#444444',
- fontSize: '12px',
- }}
- wrapperClassName="bg-gray-600 rounded-sm min-w-md"
- active={!!syncId && syncTooltip && hoveredIndex !== null}
- />
- </BarChart>
- </Container>
- {stackedData && stackedData[0] && (
- <div className="text-foreground-lighter -mt-5 flex items-center justify-between text-xs">
- <span>{formatChartDate(stackedData[0][xAxisKey] as number | string)}</span>
- <span>
- {formatChartDate(stackedData[stackedData?.length - 1][xAxisKey] as number | string)}
- </span>
- </div>
- )}
- </div>
- )
- }
- export default StackedBarChart
|