Created
May 24, 2024 02:07
-
-
Save coderdiaz/09b74826ebd0f436f6f6a846ba8b560c to your computer and use it in GitHub Desktop.
An example of BiAxialLineChart using Tremor Raw LineChart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An example of BiAxialLineChart | |
// using the Tremor Raw LineChart [v0.0.0] | |
// | |
// New props | |
// yRightCategory -> Category to show according to the yAxisRight | |
// yAxisRightLabel -> Label to be showed in yAxisRight | |
// | |
// Concerns: | |
// It's limited to only one category associated to yAxisRight if you want to support | |
// multiples categories only change it to array instead of single string. | |
"use client" | |
import React from "react" | |
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react" | |
import { | |
CartesianGrid, | |
Dot, | |
Label, | |
Line, | |
Legend as RechartsLegend, | |
LineChart as RechartsLineChart, | |
ResponsiveContainer, | |
Tooltip, | |
XAxis, | |
YAxis, | |
} from "recharts" | |
import { AxisDomain } from "recharts/types/util/types" | |
import { | |
AvailableChartColors, | |
AvailableChartColorsKeys, | |
constructCategoryColors, | |
getColorClassName, | |
getYAxisDomain, | |
hasOnlyOneValueForKey, | |
} from "@/lib/chartUtils" | |
import { useOnWindowResize } from "../../hooks/useOnWindowResize" | |
import { cn as cx } from "@/lib/utils" | |
//#region Legend | |
interface LegendItemProps { | |
name: string | |
color: AvailableChartColorsKeys | |
onClick?: (name: string, color: AvailableChartColorsKeys) => void | |
activeLegend?: string | |
} | |
const LegendItem = ({ | |
name, | |
color, | |
onClick, | |
activeLegend, | |
}: LegendItemProps) => { | |
const hasOnValueChange = !!onClick | |
return ( | |
<li | |
className={cx( | |
// base | |
"group inline-flex flex-nowrap items-center gap-1.5 whitespace-nowrap rounded px-2 py-1 transition", | |
hasOnValueChange | |
? "cursor-pointer hover:bg-accent-100" | |
: "cursor-default", | |
)} | |
onClick={(e) => { | |
e.stopPropagation() | |
onClick?.(name, color) | |
}} | |
> | |
<span | |
className={cx( | |
"h-2.5 w-2.5 shrink-0 rounded-full", | |
getColorClassName(color, "bg"), | |
activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100", | |
)} | |
aria-hidden={true} | |
/> | |
<p | |
className={cx( | |
// base | |
"truncate whitespace-nowrap text-xs", | |
// text color | |
"text-accent-500", | |
hasOnValueChange && | |
"group-hover:text-accent-900", | |
activeLegend && activeLegend !== name ? "opacity-40" : "opacity-100", | |
)} | |
> | |
{name} | |
</p> | |
</li> | |
) | |
} | |
interface ScrollButtonProps { | |
icon: React.ElementType | |
onClick?: () => void | |
disabled?: boolean | |
} | |
const ScrollButton = ({ icon, onClick, disabled }: ScrollButtonProps) => { | |
const Icon = icon | |
const [isPressed, setIsPressed] = React.useState(false) | |
const intervalRef = React.useRef<NodeJS.Timeout | null>(null) | |
React.useEffect(() => { | |
if (isPressed) { | |
intervalRef.current = setInterval(() => { | |
onClick?.() | |
}, 300) | |
} else { | |
clearInterval(intervalRef.current as NodeJS.Timeout) | |
} | |
return () => clearInterval(intervalRef.current as NodeJS.Timeout) | |
}, [isPressed, onClick]) | |
React.useEffect(() => { | |
if (disabled) { | |
clearInterval(intervalRef.current as NodeJS.Timeout) | |
setIsPressed(false) | |
} | |
}, [disabled]) | |
return ( | |
<button | |
type="button" | |
className={cx( | |
// base | |
"group inline-flex size-5 items-center truncate rounded transition", | |
disabled | |
? "cursor-not-allowed text-accent-400" | |
: "cursor-pointer text-accent-700 hover:bg-accent-100 hover:text-accent-900", | |
)} | |
disabled={disabled} | |
onClick={(e) => { | |
e.stopPropagation() | |
onClick?.() | |
}} | |
onMouseDown={(e) => { | |
e.stopPropagation() | |
setIsPressed(true) | |
}} | |
onMouseUp={(e) => { | |
e.stopPropagation() | |
setIsPressed(false) | |
}} | |
> | |
<Icon className="size-full" aria-hidden="true" /> | |
</button> | |
) | |
} | |
interface LegendProps extends React.OlHTMLAttributes<HTMLOListElement> { | |
categories: string[] | |
colors?: AvailableChartColorsKeys[] | |
onClickLegendItem?: (category: string, color: string) => void | |
activeLegend?: string | |
enableLegendSlider?: boolean | |
} | |
type HasScrollProps = { | |
left: boolean | |
right: boolean | |
} | |
const Legend = React.forwardRef<HTMLOListElement, LegendProps>((props, ref) => { | |
const { | |
categories, | |
colors = AvailableChartColors, | |
className, | |
onClickLegendItem, | |
activeLegend, | |
enableLegendSlider = false, | |
...other | |
} = props | |
const scrollableRef = React.useRef<HTMLInputElement>(null) | |
const [hasScroll, setHasScroll] = React.useState<HasScrollProps | null>(null) | |
const [isKeyDowned, setIsKeyDowned] = React.useState<string | null>(null) | |
const intervalRef = React.useRef<NodeJS.Timeout | null>(null) | |
const checkScroll = React.useCallback(() => { | |
const scrollable = scrollableRef?.current | |
if (!scrollable) return | |
const hasLeftScroll = scrollable.scrollLeft > 0 | |
const hasRightScroll = | |
scrollable.scrollWidth - scrollable.clientWidth > scrollable.scrollLeft | |
setHasScroll({ left: hasLeftScroll, right: hasRightScroll }) | |
}, [setHasScroll]) | |
const scrollToTest = React.useCallback( | |
(direction: "left" | "right") => { | |
const element = scrollableRef?.current | |
const width = element?.clientWidth ?? 0 | |
if (element && enableLegendSlider) { | |
element.scrollTo({ | |
left: | |
direction === "left" | |
? element.scrollLeft - width | |
: element.scrollLeft + width, | |
behavior: "smooth", | |
}) | |
setTimeout(() => { | |
checkScroll() | |
}, 400) | |
} | |
}, | |
[enableLegendSlider, checkScroll], | |
) | |
React.useEffect(() => { | |
const keyDownHandler = (key: string) => { | |
if (key === "ArrowLeft") { | |
scrollToTest("left") | |
} else if (key === "ArrowRight") { | |
scrollToTest("right") | |
} | |
} | |
if (isKeyDowned) { | |
keyDownHandler(isKeyDowned) | |
intervalRef.current = setInterval(() => { | |
keyDownHandler(isKeyDowned) | |
}, 300) | |
} else { | |
clearInterval(intervalRef.current as NodeJS.Timeout) | |
} | |
return () => clearInterval(intervalRef.current as NodeJS.Timeout) | |
}, [isKeyDowned, scrollToTest]) | |
const keyDown = (e: KeyboardEvent) => { | |
e.stopPropagation() | |
if (e.key === "ArrowLeft" || e.key === "ArrowRight") { | |
e.preventDefault() | |
setIsKeyDowned(e.key) | |
} | |
} | |
const keyUp = (e: KeyboardEvent) => { | |
e.stopPropagation() | |
setIsKeyDowned(null) | |
} | |
React.useEffect(() => { | |
const scrollable = scrollableRef?.current | |
if (enableLegendSlider) { | |
checkScroll() | |
scrollable?.addEventListener("keydown", keyDown) | |
scrollable?.addEventListener("keyup", keyUp) | |
} | |
return () => { | |
scrollable?.removeEventListener("keydown", keyDown) | |
scrollable?.removeEventListener("keyup", keyUp) | |
} | |
}, [checkScroll, enableLegendSlider]) | |
return ( | |
<ol | |
ref={ref} | |
className={cx("relative overflow-hidden", className)} | |
{...other} | |
> | |
<div | |
ref={scrollableRef} | |
tabIndex={0} | |
className={cx( | |
"flex h-full", | |
enableLegendSlider | |
? hasScroll?.right || hasScroll?.left | |
? "snap-mandatory items-center overflow-auto pl-4 pr-12 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" | |
: "" | |
: "flex-wrap", | |
)} | |
> | |
{categories.map((category, index) => ( | |
<LegendItem | |
key={`item-${index}`} | |
name={category} | |
color={colors[index] as AvailableChartColorsKeys} | |
onClick={onClickLegendItem} | |
activeLegend={activeLegend} | |
/> | |
))} | |
</div> | |
{enableLegendSlider && (hasScroll?.right || hasScroll?.left) ? ( | |
<> | |
<div | |
className={cx( | |
// base | |
"absolute bottom-0 right-0 top-0 flex h-full items-center justify-center pr-1", | |
// background color | |
"bg-accent-1100", | |
)} | |
> | |
<ScrollButton | |
icon={ArrowLeftIcon} | |
onClick={() => { | |
setIsKeyDowned(null) | |
scrollToTest("left") | |
}} | |
disabled={!hasScroll?.left} | |
/> | |
<ScrollButton | |
icon={ArrowRightIcon} | |
onClick={() => { | |
setIsKeyDowned(null) | |
scrollToTest("right") | |
}} | |
disabled={!hasScroll?.right} | |
/> | |
</div> | |
</> | |
) : null} | |
</ol> | |
) | |
}) | |
Legend.displayName = "Legend" | |
const ChartLegend = ( | |
{ payload }: any, | |
categoryColors: Map<string, AvailableChartColorsKeys>, | |
setLegendHeight: React.Dispatch<React.SetStateAction<number>>, | |
activeLegend: string | undefined, | |
onClick?: (category: string, color: string) => void, | |
enableLegendSlider?: boolean, | |
) => { | |
const legendRef = React.useRef<HTMLDivElement>(null) | |
useOnWindowResize(() => { | |
const calculateHeight = (height: number | undefined) => | |
height ? Number(height) + 15 : 60 | |
setLegendHeight(calculateHeight(legendRef.current?.clientHeight)) | |
}) | |
const filteredPayload = payload.filter((item: any) => item.type !== "none") | |
return ( | |
<div ref={legendRef} className="flex items-center justify-end"> | |
<Legend | |
categories={filteredPayload.map((entry: any) => entry.value)} | |
colors={filteredPayload.map((entry: any) => | |
categoryColors.get(entry.value), | |
)} | |
onClickLegendItem={onClick} | |
activeLegend={activeLegend} | |
enableLegendSlider={enableLegendSlider} | |
/> | |
</div> | |
) | |
} | |
//#region Tooltip | |
interface ChartTooltipRowProps { | |
value: string | |
name: string | |
color: string | |
} | |
const ChartTooltipRow = ({ value, name, color }: ChartTooltipRowProps) => ( | |
<div className="flex items-center justify-between space-x-8"> | |
<div className="flex items-center space-x-2"> | |
<span | |
aria-hidden="true" | |
className={cx("h-[3px] w-3.5 shrink-0 rounded-full", color)} | |
/> | |
<p | |
className={cx( | |
// commmon | |
"whitespace-nowrap text-right", | |
// text color | |
"text-accent-500", | |
)} | |
> | |
{name} | |
</p> | |
</div> | |
<p | |
className={cx( | |
// base | |
"whitespace-nowrap text-right font-medium tabular-nums", | |
// text color | |
"text-accent-400", | |
)} | |
> | |
{value} | |
</p> | |
</div> | |
) | |
interface ChartTooltipProps { | |
active: boolean | undefined | |
payload: any | |
label: string | |
categoryColors: Map<string, string> | |
valueFormatter: (value: number) => string | |
} | |
const ChartTooltip = ({ | |
active, | |
payload, | |
label, | |
categoryColors, | |
valueFormatter, | |
}: ChartTooltipProps) => { | |
if (active && payload) { | |
const filteredPayload = payload.filter((item: any) => item.type !== "none") | |
return ( | |
<div | |
className={cx( | |
// base | |
"rounded-md border text-sm shadow-md", | |
// border color | |
"border-accent-1100", | |
// background color | |
"bg-accent-1200", | |
)} | |
> | |
<div | |
className={cx( | |
// base | |
"border-b border-accent-1000 px-4 py-2", | |
)} | |
> | |
<p | |
className={cx( | |
// base | |
"font-medium", | |
// text color | |
"text-accent-500", | |
)} | |
> | |
{label} | |
</p> | |
</div> | |
<div className={cx("space-y-1 px-4 py-2")}> | |
{filteredPayload.map( | |
( | |
{ value, name }: { value: number; name: string }, | |
index: number, | |
) => ( | |
<ChartTooltipRow | |
key={`id-${index}`} | |
value={valueFormatter(value)} | |
name={name} | |
color={getColorClassName( | |
categoryColors.get(name) as AvailableChartColorsKeys, | |
"bg", | |
)} | |
/> | |
), | |
)} | |
</div> | |
</div> | |
) | |
} | |
return null | |
} | |
//#region LineChart | |
interface ActiveDot { | |
index?: number | |
dataKey?: string | |
} | |
type BaseEventProps = { | |
eventType: "dot" | "category" | |
categoryClicked: string | |
[key: string]: number | string | |
} | |
type LineChartEventProps = BaseEventProps | null | undefined | |
export interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> { | |
data: Record<string, any>[] | |
index: string | |
categories: string[] | |
colors?: AvailableChartColorsKeys[] | |
valueFormatter?: (value: number) => string | |
startEndOnly?: boolean | |
showXAxis?: boolean | |
showYAxis?: boolean | |
showGridLines?: boolean | |
yAxisWidth?: number | |
intervalType?: "preserveStartEnd" | "equidistantPreserveStart" | |
showTooltip?: boolean | |
showLegend?: boolean | |
autoMinValue?: boolean | |
minValue?: number | |
maxValue?: number | |
allowDecimals?: boolean | |
onValueChange?: (value: LineChartEventProps) => void | |
enableLegendSlider?: boolean | |
tickGap?: number | |
connectNulls?: boolean | |
xAxisLabel?: string | |
yAxisLabel?: string | |
yRightCategory?: string | |
yAxisRightLabel?: string | |
} | |
const BiAxialLineChart = React.forwardRef<HTMLDivElement, LineChartProps>( | |
(props, ref) => { | |
const { | |
data = [], | |
categories = [], | |
index, | |
colors = AvailableChartColors, | |
valueFormatter = (value: number) => value.toString(), | |
startEndOnly = false, | |
showXAxis = true, | |
showYAxis = true, | |
showGridLines = true, | |
yAxisWidth = 56, | |
intervalType = "equidistantPreserveStart", | |
showTooltip = true, | |
showLegend = true, | |
autoMinValue = false, | |
minValue, | |
maxValue, | |
allowDecimals = true, | |
connectNulls = false, | |
className, | |
onValueChange, | |
enableLegendSlider = false, | |
tickGap = 5, | |
xAxisLabel, | |
yAxisLabel, | |
yRightCategory, | |
yAxisRightLabel, | |
...other | |
} = props | |
const paddingValue = !showXAxis && !showYAxis ? 0 : 20 | |
const [legendHeight, setLegendHeight] = React.useState(60) | |
const [activeDot, setActiveDot] = React.useState<ActiveDot | undefined>( | |
undefined, | |
) | |
const [activeLegend, setActiveLegend] = React.useState<string | undefined>( | |
undefined, | |
) | |
const categoryColors = constructCategoryColors(categories, colors) | |
const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue) | |
const hasOnValueChange = !!onValueChange | |
function onDotClick(itemData: any, event: React.MouseEvent) { | |
event.stopPropagation() | |
if (!hasOnValueChange) return | |
if ( | |
(itemData.index === activeDot?.index && | |
itemData.dataKey === activeDot?.dataKey) || | |
(hasOnlyOneValueForKey(data, itemData.dataKey) && | |
activeLegend && | |
activeLegend === itemData.dataKey) | |
) { | |
setActiveLegend(undefined) | |
setActiveDot(undefined) | |
onValueChange?.(null) | |
} else { | |
setActiveLegend(itemData.dataKey) | |
setActiveDot({ | |
index: itemData.index, | |
dataKey: itemData.dataKey, | |
}) | |
onValueChange?.({ | |
eventType: "dot", | |
categoryClicked: itemData.dataKey, | |
...itemData.payload, | |
}) | |
} | |
} | |
function onCategoryClick(dataKey: string) { | |
if (!hasOnValueChange) return | |
if ( | |
(dataKey === activeLegend && !activeDot) || | |
(hasOnlyOneValueForKey(data, dataKey) && | |
activeDot && | |
activeDot.dataKey === dataKey) | |
) { | |
setActiveLegend(undefined) | |
onValueChange?.(null) | |
} else { | |
setActiveLegend(dataKey) | |
onValueChange?.({ | |
eventType: "category", | |
categoryClicked: dataKey, | |
}) | |
} | |
setActiveDot(undefined) | |
} | |
return ( | |
<div ref={ref} className={cx("h-80 w-full", className)} {...other}> | |
<ResponsiveContainer> | |
<RechartsLineChart | |
data={data} | |
onClick={ | |
hasOnValueChange && (activeLegend || activeDot) | |
? () => { | |
setActiveDot(undefined) | |
setActiveLegend(undefined) | |
onValueChange?.(null) | |
} | |
: undefined | |
} | |
margin={{ | |
bottom: xAxisLabel ? 30 : undefined, | |
left: yAxisLabel ? 0 : undefined, | |
right: yAxisLabel ? 5 : undefined, | |
top: 5, | |
}} | |
> | |
{showGridLines ? ( | |
<CartesianGrid | |
className={cx("stroke-accent-1000 stroke-1")} | |
horizontal={true} | |
vertical={false} | |
/> | |
) : null} | |
<XAxis | |
padding={{ left: paddingValue, right: paddingValue }} | |
hide={!showXAxis} | |
dataKey={index} | |
interval={startEndOnly ? "preserveStartEnd" : intervalType} | |
tick={{ transform: "translate(0, 6)" }} | |
ticks={ | |
startEndOnly | |
? [data[0][index], data[data.length - 1][index]] | |
: undefined | |
} | |
fill="" | |
stroke="" | |
className={cx( | |
// base | |
"text-xs", | |
// text fill | |
"fill-accent-700", | |
)} | |
tickLine={false} | |
axisLine={false} | |
minTickGap={tickGap} | |
> | |
{xAxisLabel && ( | |
<Label | |
position="insideBottom" | |
offset={-20} | |
className="fill-accent-800 text-sm font-medium" | |
> | |
{xAxisLabel} | |
</Label> | |
)} | |
</XAxis> | |
<YAxis | |
width={yAxisWidth} | |
hide={!showYAxis} | |
axisLine={false} | |
tickLine={false} | |
yAxisId="left" | |
type="number" | |
domain={yAxisDomain as AxisDomain} | |
tick={{ transform: "translate(-3, 0)" }} | |
fill="" | |
stroke="" | |
className={cx( | |
// base | |
"text-xs", | |
// text fill | |
"fill-accent-700", | |
)} | |
tickFormatter={valueFormatter} | |
allowDecimals={allowDecimals} | |
> | |
{yAxisLabel && ( | |
<Label | |
position="insideLeft" | |
style={{ textAnchor: "middle" }} | |
angle={-90} | |
offset={6} | |
className="fill-accent-800 text-sm font-medium" | |
> | |
{yAxisLabel} | |
</Label> | |
)} | |
</YAxis> | |
{yRightCategory && ( | |
<YAxis | |
yAxisId="right" | |
orientation="right" | |
width={yAxisWidth} | |
hide={!showYAxis} | |
axisLine={false} | |
tickLine={false} | |
type="number" | |
domain={yAxisDomain as AxisDomain} | |
tick={{ transform: "translate(-3, 0)" }} | |
fill="" | |
stroke="" | |
className={cx( | |
// base | |
"text-xs", | |
// text fill | |
"fill-accent-700", | |
)} | |
tickFormatter={valueFormatter} | |
allowDecimals={allowDecimals} | |
> | |
{yAxisRightLabel && ( | |
<Label | |
position="insideRight" | |
style={{ textAnchor: "middle" }} | |
angle={90} | |
offset={4} | |
className="fill-accent-800 text-sm font-medium" | |
> | |
{yAxisRightLabel} | |
</Label> | |
)} | |
</YAxis> | |
)} | |
<Tooltip | |
wrapperStyle={{ outline: "none" }} | |
isAnimationActive={true} | |
animationDuration={100} | |
cursor={{ stroke: "#4A2D2F", strokeWidth: 1 }} | |
offset={20} | |
position={{ y: 0 }} | |
content={ | |
showTooltip ? ( | |
({ active, payload, label }) => ( | |
<ChartTooltip | |
active={active} | |
payload={payload} | |
label={label} | |
valueFormatter={valueFormatter} | |
categoryColors={categoryColors} | |
/> | |
) | |
) : ( | |
<></> | |
) | |
} | |
/> | |
{showLegend ? ( | |
<RechartsLegend | |
verticalAlign="top" | |
height={legendHeight} | |
content={({ payload }) => | |
ChartLegend( | |
{ payload }, | |
categoryColors, | |
setLegendHeight, | |
activeLegend, | |
hasOnValueChange | |
? (clickedLegendItem: string) => | |
onCategoryClick(clickedLegendItem) | |
: undefined, | |
enableLegendSlider, | |
) | |
} | |
/> | |
) : null} | |
{categories.map((category) => ( | |
<Line | |
className={cx( | |
getColorClassName( | |
categoryColors.get(category) as AvailableChartColorsKeys, | |
"stroke", | |
), | |
)} | |
strokeOpacity={ | |
activeDot || (activeLegend && activeLegend !== category) | |
? 0.3 | |
: 1 | |
} | |
activeDot={(props: any) => { | |
const { | |
cx: cxCoord, | |
cy: cyCoord, | |
stroke, | |
strokeLinecap, | |
strokeLinejoin, | |
strokeWidth, | |
dataKey, | |
} = props | |
return ( | |
<Dot | |
className={cx( | |
"stroke-accent-1200", | |
onValueChange ? "cursor-pointer" : "", | |
getColorClassName( | |
categoryColors.get( | |
dataKey, | |
) as AvailableChartColorsKeys, | |
"fill", | |
), | |
)} | |
cx={cxCoord} | |
cy={cyCoord} | |
r={5} | |
fill="" | |
stroke={stroke} | |
strokeLinecap={strokeLinecap} | |
strokeLinejoin={strokeLinejoin} | |
strokeWidth={strokeWidth} | |
onClick={(_, event) => onDotClick(props, event)} | |
/> | |
) | |
}} | |
dot={(props: any) => { | |
const { | |
stroke, | |
strokeLinecap, | |
strokeLinejoin, | |
strokeWidth, | |
cx: cxCoord, | |
cy: cyCoord, | |
dataKey, | |
index, | |
} = props | |
if ( | |
(hasOnlyOneValueForKey(data, category) && | |
!( | |
activeDot || | |
(activeLegend && activeLegend !== category) | |
)) || | |
(activeDot?.index === index && | |
activeDot?.dataKey === category) | |
) { | |
return ( | |
<Dot | |
key={index} | |
cx={cxCoord} | |
cy={cyCoord} | |
r={5} | |
stroke={stroke} | |
fill="" | |
strokeLinecap={strokeLinecap} | |
strokeLinejoin={strokeLinejoin} | |
strokeWidth={strokeWidth} | |
className={cx( | |
"stroke-accent-1200", | |
onValueChange ? "cursor-pointer" : "", | |
getColorClassName( | |
categoryColors.get( | |
dataKey, | |
) as AvailableChartColorsKeys, | |
"fill", | |
), | |
)} | |
/> | |
) | |
} | |
return <React.Fragment key={index}></React.Fragment> | |
}} | |
key={category} | |
name={category} | |
type="linear" | |
{...(category === yRightCategory ? { yAxisId: "right" } : {yAxisId: "left"})} | |
dataKey={category} | |
stroke="" | |
strokeWidth={2} | |
strokeLinejoin="round" | |
strokeLinecap="round" | |
isAnimationActive={false} | |
connectNulls={connectNulls} | |
/> | |
))} | |
{/* hidden lines to increase clickable target area */} | |
{onValueChange | |
? categories.map((category) => ( | |
<Line | |
className={cx("cursor-pointer")} | |
strokeOpacity={0} | |
key={category} | |
name={category} | |
type="linear" | |
{...(category === yRightCategory ? { yAxisId: "right" } : {yAxisId: "left"})} | |
dataKey={category} | |
stroke="transparent" | |
fill="transparent" | |
legendType="none" | |
tooltipType="none" | |
strokeWidth={12} | |
connectNulls={connectNulls} | |
onClick={(props: any, event) => { | |
event.stopPropagation() | |
const { name } = props | |
onCategoryClick(name) | |
}} | |
/> | |
)) | |
: null} | |
</RechartsLineChart> | |
</ResponsiveContainer> | |
</div> | |
) | |
}, | |
) | |
BiAxialLineChart.displayName = "BiAxialLineChart" | |
export { BiAxialLineChart, type LineChartEventProps } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment