Closed technoligest closed 4 months ago
And I think it would be better if the Y-axis of the chart could support setting a specific base value instead of being fixed at 0. This would improve scalability, similar to the profit curve chart in TradingView.
Hey @technoligest! I presume this is the AreaChart component that you are referring to. Do you have any suggestions about how multiple areas should be mapped? For example, in the above graph, we are demonstrating two area graphs in the single component. Using red for the values below baseline for all the charts would make it really untidy.
Hey @technoligest! I presume this is the AreaChart component that you are referring to. Do you have any suggestions about how multiple areas should be mapped? For example, in the above graph, we are demonstrating two area graphs in the single component. Using red for the values below baseline for all the charts would make it really untidy.
Your graph is a little different. It's multiple lines on the same graph, but it always "switches" on the 0 y-axis. I think there should be an option to select which y-axis the "switch" should happen on.
@technoligest Yeah, I understand that part. I was trying to clarify what should the case be when there are multiple lines? What kind of color should we use to denote the negative part?
Or if I am misunderstanding your feature request, do correct me. :D
@rajdip-b - I can't speak for the other posters, but for my orgs use case, we never need to show multiple line charts together when we want to visualize a positive <-> negative representation. I would guess most use cases would be a single line over multiple since that can easily cause visual confusion.
Hi @technoligest, @zaleGZL, @randallmlough, @randallmlough, @rajdip-b ! It's been quite a while. @randallmlough last comment is a very good answer to this issue. There are very little use-cases for this (stocks), but in all other cases, a bar chart would be a better solution. So we won't add this to our library.
However, I tinkered on this issue a few weeks back over at Tremor Raw (our copy & paste components). I am still not convinced of this chart type, that's why we have not released it.
Anyway, I'll post the code for this here (you need to install the utilities from tremor raw):
"use client"
import React from "react"
import { RiArrowLeftSLine, RiArrowRightSLine } from "@remixicon/react"
import {
Area,
CartesianGrid,
Dot,
Label,
Line,
AreaChart as RechartsAreaChart,
Legend as RechartsLegend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts"
import { AxisDomain } from "recharts/types/util/types"
import { getYAxisDomain, hasOnlyOneValueForKey } from "@/lib/chartUtils"
import { useOnWindowResize } from "@/lib/useOnWindowResize"
import { cx } from "@/lib/utils"
//#region chartColors
export type ColorUtility = "bg" | "stroke" | "fill" | "text"
export const chartColors = {
split: {
bg: "bg-gray-700 dark:bg-gray-50",
stroke: "stroke-gray-700 dark:stroke-gray-50",
fill: "fill-gray-700 dark:fill-gray-50",
text: "text-gray-700 dark:text-gray-50",
},
} as const satisfies {
[color: string]: {
[key in ColorUtility]: string
}
}
export type AvailableChartColorsKeys = keyof typeof chartColors
export const AvailableChartColors: AvailableChartColorsKeys[] = Object.keys(
chartColors,
) as Array<AvailableChartColorsKeys>
export const constructCategoryColors = (
categories: string[],
colors: AvailableChartColorsKeys[],
): Map<string, AvailableChartColorsKeys> => {
const categoryColors = new Map<string, AvailableChartColorsKeys>()
categories.forEach((category, index) => {
categoryColors.set(category, colors[index % colors.length])
})
return categoryColors
}
export const getColorClassName = (
color: AvailableChartColorsKeys,
type: ColorUtility,
): string => {
const fallbackColor = {
bg: "bg-gray-500",
stroke: "stroke-gray-500",
fill: "fill-gray-500",
text: "text-gray-500",
}
return chartColors[color]?.[type] ?? fallbackColor[type]
}
//#region SplitOffset
function getSplitOffset({
data,
category,
}: {
data: Record<string, any>[]
category: string
}) {
const dataMax = Math.max(...data.map((item) => item[category]))
const dataMin = Math.min(...data.map((item) => item[category]))
if (dataMax <= 0) {
return 0
}
if (dataMin >= 0) {
return 1
}
return dataMax / (dataMax - dataMin)
}
//#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-gray-100 dark:hover:bg-gray-800"
: "cursor-default",
)}
onClick={(e) => {
e.stopPropagation()
onClick?.(name, color)
}}
>
<span
className={cx(
"h-[3px] w-3.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-gray-700 dark:text-gray-300",
hasOnValueChange &&
"group-hover:text-gray-900 dark:group-hover:text-gray-50",
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-gray-400 dark:text-gray-600"
: "cursor-pointer text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-50",
)}
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-white dark:bg-gray-950",
)}
>
<ScrollButton
icon={RiArrowLeftSLine}
onClick={() => {
setIsKeyDowned(null)
scrollToTest("left")
}}
disabled={!hasScroll?.left}
/>
<ScrollButton
icon={RiArrowRightSLine}
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-gray-700 dark:text-gray-300",
)}
>
{name}
</p>
</div>
<p
className={cx(
// base
"whitespace-nowrap text-right font-medium tabular-nums",
// text color
"text-gray-900 dark:text-gray-50",
)}
>
{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-gray-200 dark:border-gray-800",
// background color
"bg-white dark:bg-gray-950",
)}
>
<div
className={cx(
// base
"border-b border-inherit px-4 py-2",
)}
>
<p
className={cx(
// base
"font-medium",
// text color
"text-gray-900 dark:text-gray-50",
)}
>
{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 AreaSplitChart
interface ActiveDot {
index?: number
dataKey?: string
}
type BaseEventProps = {
eventType: "dot" | "category"
categoryClicked: string
[key: string]: number | string
}
type AreaChartEventProps = BaseEventProps | null | undefined
interface AreaChartProps 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: AreaChartEventProps) => void
enableLegendSlider?: boolean
tickGap?: number
connectNulls?: boolean
xAxisLabel?: string
yAxisLabel?: string
splitColors?: [string, string]
splitOffset?: number
}
const AreaSplitChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
(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,
splitColors = ["#10b981", "#ef4444"],
splitOffset,
...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 splitId = `${React.useId()}-split`
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>
<RechartsAreaChart
data={data}
onClick={
hasOnValueChange && (activeLegend || activeDot)
? () => {
setActiveDot(undefined)
setActiveLegend(undefined)
onValueChange?.(null)
}
: undefined
}
margin={{
bottom: xAxisLabel ? 30 : undefined,
left: yAxisLabel ? 20 : undefined,
right: yAxisLabel ? 5 : undefined,
top: 5,
}}
>
{showGridLines ? (
<CartesianGrid
className={cx("stroke-gray-200 stroke-1 dark:stroke-gray-800")}
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-gray-500 dark:fill-gray-500",
)}
tickLine={false}
axisLine={false}
minTickGap={tickGap}
>
{xAxisLabel && (
<Label
position="insideBottom"
offset={-20}
className="fill-gray-800 text-sm font-medium dark:fill-gray-200"
>
{xAxisLabel}
</Label>
)}
</XAxis>
<YAxis
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-gray-500 dark:fill-gray-500",
)}
tickFormatter={valueFormatter}
allowDecimals={allowDecimals}
>
{yAxisLabel && (
<Label
position="insideLeft"
style={{ textAnchor: "middle" }}
angle={-90}
offset={-15}
className="fill-gray-800 text-sm font-medium dark:fill-gray-200"
>
{yAxisLabel}
</Label>
)}
</YAxis>
<Tooltip
wrapperStyle={{ outline: "none" }}
isAnimationActive={true}
animationDuration={100}
cursor={{ stroke: "#d1d5db", 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) => (
<defs key={category}>
<linearGradient id={splitId} x1="0" y1="0" x2="0" y2="1">
<stop
offset={
splitOffset ??
getSplitOffset({ data: data, category: category })
}
stopColor={splitColors[0]}
stopOpacity={
activeDot || (activeLegend && activeLegend !== category)
? 0.15
: 0.3
}
/>
<stop
offset={
splitOffset ??
getSplitOffset({ data: data, category: category })
}
stopColor={splitColors[1]}
stopOpacity={
activeDot || (activeLegend && activeLegend !== category)
? 0.15
: 0.3
}
/>
</linearGradient>
</defs>
))}
{categories.map((category) => (
<Area
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-white dark:stroke-gray-950",
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-white dark:stroke-gray-950",
onValueChange ? "cursor-pointer" : "",
getColorClassName(
categoryColors.get(
dataKey,
) as AvailableChartColorsKeys,
"fill",
),
)}
/>
)
}
return <React.Fragment key={index}></React.Fragment>
}}
key={category}
name={category}
type="linear"
dataKey={category}
stroke=""
strokeWidth={2}
strokeLinejoin="round"
strokeLinecap="round"
isAnimationActive={false}
connectNulls={connectNulls}
fill={`url(#${splitId})`}
/>
))}
{/* hidden lines to increase clickable target area */}
{onValueChange
? categories.map((category) => (
<Line
className={cx("cursor-pointer")}
strokeOpacity={0}
key={category}
name={category}
type="linear"
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}
</RechartsAreaChart>
</ResponsiveContainer>
</div>
)
},
)
AreaSplitChart.displayName = "AreaSplitChart"
export { AreaSplitChart, type AreaChartEventProps }
const chartdata = [
{
date: "Jan 23",
ProfitLoss: 5000,
},
{
date: "Feb 23",
ProfitLoss: 4500,
},
{
date: "Mar 23",
ProfitLoss: 7000,
},
{
date: "Apr 23",
ProfitLoss: 6000,
},
{
date: "May 23",
ProfitLoss: 7500,
},
{
date: "Jun 23",
ProfitLoss: 7000,
},
{
date: "Jul 23",
ProfitLoss: 3500,
},
{
date: "Aug 23",
ProfitLoss: -1000,
},
{
date: "Sep 23",
ProfitLoss: -2000,
},
{
date: "Oct 23",
ProfitLoss: -1500,
},
{
date: "Nov 23",
ProfitLoss: -2500,
},
{
date: "Dec 23",
ProfitLoss: -4000,
},
]
export const AreaSplitChartExample = () => (
<AreaSplitChart
className="h-80"
yAxisWidth={60}
data={chartdata}
index="date"
categories={["ProfitLoss"]}
valueFormatter={(number: number) =>
`$${Intl.NumberFormat("us").format(number).toString()}`
}
onValueChange={(v) => console.log(v)}
/>
)
What problem does this feature solve?
Showing if a value went below a given threshold (positive to negative for example) over time.
It would be great if the graph below switched colors as it went below and above 0.
What does the proposed API look like?
2 colors + threshold value for switching the graph.