Open Dib5pm opened 3 weeks ago
I managed to solve this issue by using the reversed
prop on the YAxis
component within Recharts to invert the axis. Additionally, I configured the Area
component's baseValue
to 'dataMax'
, ensuring that the area fills downward from the maximum data point on the Y-axis. I've already added the code below so you can implement it yourself. I'll make a PR when I can find the time.
There is a known bug in the Recharts library that affects the visibility of the full Y and X axes. This issue can result in the axes not being fully displayed, which impacts the readability and presentation of the chart data. More details about this issue can be found in the Recharts GitHub repository under Issue #2175.
I managed to solve this by adding:
margin={{
top: 10, right: 30, left: 0, bottom: 0,
}}
Below is a simple example illustrating how these properties can be integrated in Recharts:
import React, { PureComponent } from 'react';
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
const data = [
{ name: 'Page A', uv: 4000 },
{ name: 'Page B', uv: 3000 },
{ name: 'Page C', uv: 2000 },
{ name: 'Page D', uv: 2780 },
{ name: 'Page E', uv: 1890 },
{ name: 'Page F', uv: 2390 },
{ name: 'Page G', uv: 3490 },
];
export default class Example extends PureComponent {
render() {
return (
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{
top: 10, right: 30, left: 0, bottom: 0,
}}
>
<XAxis dataKey="name" />
<YAxis reversed domain={[Math.max(...data.map(item => item.uv)), Math.min(...data.map(item => item.uv))]} />
<Tooltip />
<Area type="monotone" dataKey="uv" baseValue='dataMax' stroke="#8884d8" fill="#8884d8" />
</AreaChart>
</ResponsiveContainer>
);
}
}
import React, { Fragment, useState } from 'react';
import {
Area,
AreaChart as ReChartsAreaChart,
CartesianGrid,
Dot,
Legend,
Line,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import BaseChartProps from './common/BaseChartProps';
import { CurveType } from './lib/inputTypes';
import { colorPalette, themeColorRange } from './lib/theme';
import {
constructCategoryColors,
defaultValueFormatter,
getColorClassNames,
getYAxisDomain,
hasOnlyOneValueForThisKey,
} from './lib/utils';
import { tremorTwMerge } from './lib/tremorTwMerge';
import ChartTooltip from './common/ChartTooltip';
import { BaseColors } from './lib/constants';
import ChartLegend from './common/ChartLegend';
import NoData from './common/NoData';
import { AxisDomain } from 'recharts/types/util/types';
export interface AreaChartProps extends BaseChartProps {
stack?: boolean;
curveType?: CurveType;
connectNulls?: boolean;
yAxisReversed?: boolean;
showGradient?: boolean;
}
interface ActiveDot {
index?: number;
dataKey?: string;
}
const AreaChart = React.forwardRef<HTMLDivElement, AreaChartProps>(
(props, ref) => {
const {
data = [],
categories = [],
index,
stack = false,
colors = themeColorRange,
valueFormatter = defaultValueFormatter,
startEndOnly = false,
showXAxis = true,
showYAxis = true,
yAxisWidth = 56,
yAxisReversed = false,
intervalType = 'equidistantPreserveStart',
showAnimation = false,
animationDuration = 900,
showTooltip = true,
showLegend = true,
showGridLines = true,
showGradient = true,
autoMinValue = false,
curveType = 'linear',
minValue,
maxValue,
connectNulls = false,
allowDecimals = true,
noDataText,
className,
onValueChange,
enableLegendSlider = false,
customTooltip,
rotateLabelX,
tickGap = 5,
...other
} = props;
const CustomTooltip = customTooltip;
const paddingValue =
(!showXAxis && !showYAxis) || (startEndOnly && !showYAxis) ? 0 : 20;
const [legendHeight, setLegendHeight] = useState(60);
const [activeDot, setActiveDot] = useState<ActiveDot | undefined>(
undefined
);
const [activeLegend, setActiveLegend] = useState<string | undefined>(
undefined
);
const categoryColors = constructCategoryColors(categories, colors);
const yAxisDomain = getYAxisDomain(autoMinValue, minValue, maxValue);
const yAxisProps = yAxisReversed && { reversed: true };
const areaProps = yAxisReversed && { baseValue: 'dataMax' as const };
const hasOnValueChange = !!onValueChange;
function onDotClick(itemData: any, event: React.MouseEvent) {
event.stopPropagation();
if (!hasOnValueChange) return;
if (
(itemData.index === activeDot?.index &&
itemData.dataKey === activeDot?.dataKey) ||
(hasOnlyOneValueForThisKey(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) ||
(hasOnlyOneValueForThisKey(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={tremorTwMerge('w-full h-80', className)}
{...other}
>
<ResponsiveContainer className="h-full w-full">
{data?.length ? (
<ReChartsAreaChart
{...(yAxisReversed && {
margin: {
top: 10,
right: 30,
left: 0,
bottom: 0,
},
})}
data={data}
onClick={
hasOnValueChange && (activeLegend || activeDot)
? () => {
setActiveDot(undefined);
setActiveLegend(undefined);
onValueChange?.(null);
}
: undefined
}
>
{showGridLines ? (
<CartesianGrid
className={tremorTwMerge(
// common
'stroke-1',
// light
'stroke-tremor-border',
// dark
'dark:stroke-dark-tremor-border'
)}
horizontal={true}
vertical={false}
/>
) : null}
<XAxis
padding={{ left: paddingValue, right: paddingValue }}
hide={!showXAxis}
dataKey={index}
tick={{ transform: 'translate(0, 6)' }}
ticks={
startEndOnly
? [data[0][index], data[data.length - 1][index]]
: undefined
}
fill=""
stroke=""
className={tremorTwMerge(
// common
'text-tremor-label',
// light
'fill-tremor-content',
// dark
'dark:fill-dark-tremor-content'
)}
interval={startEndOnly ? 'preserveStartEnd' : intervalType}
tickLine={false}
axisLine={false}
minTickGap={tickGap}
angle={rotateLabelX?.angle}
dy={rotateLabelX?.verticalShift}
height={rotateLabelX?.xAxisHeight}
/>
<YAxis
width={yAxisWidth}
hide={!showYAxis}
axisLine={false}
tickLine={false}
type="number"
{...yAxisProps}
domain={yAxisDomain as AxisDomain}
tick={{ transform: 'translate(-3, 0)' }}
fill=""
stroke=""
className={tremorTwMerge(
// common
'text-tremor-label',
// light
'fill-tremor-content',
// dark
'dark:fill-dark-tremor-content'
)}
tickFormatter={valueFormatter}
allowDecimals={allowDecimals}
/>
<Tooltip
wrapperStyle={{ outline: 'none' }}
isAnimationActive={false}
cursor={{ stroke: '#d1d5db', strokeWidth: 1 }}
content={
showTooltip ? (
({ active, payload, label }) =>
CustomTooltip ? (
<CustomTooltip
payload={payload?.map((payloadItem: any) => ({
...payloadItem,
color:
categoryColors.get(payloadItem.dataKey) ??
BaseColors.Gray,
}))}
active={active}
label={label}
/>
) : (
<ChartTooltip
active={active}
payload={payload}
label={label}
valueFormatter={valueFormatter}
categoryColors={categoryColors}
/>
)
) : (
<></>
)
}
position={{ y: 0 }}
/>
{showLegend ? (
<Legend
verticalAlign="top"
height={legendHeight}
content={({ payload }) =>
ChartLegend(
{ payload },
categoryColors,
setLegendHeight,
activeLegend,
hasOnValueChange
? (clickedLegendItem: string) =>
onCategoryClick(clickedLegendItem)
: undefined,
enableLegendSlider
)
}
/>
) : null}
{categories.map((category) => {
return (
<defs key={category}>
{showGradient ? (
<linearGradient
className={
getColorClassNames(
categoryColors.get(category) ?? BaseColors.Gray,
colorPalette.text
).textColor
}
id={categoryColors.get(category)}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor="currentColor"
stopOpacity={
activeDot ||
(activeLegend && activeLegend !== category)
? 0.15
: 0.4
}
/>
<stop
offset="95%"
stopColor="currentColor"
stopOpacity={0}
/>
</linearGradient>
) : (
<linearGradient
className={
getColorClassNames(
categoryColors.get(category) ?? BaseColors.Gray,
colorPalette.text
).textColor
}
id={categoryColors.get(category)}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
stopColor="currentColor"
stopOpacity={
activeDot ||
(activeLegend && activeLegend !== category)
? 0.1
: 0.3
}
/>
</linearGradient>
)}
</defs>
);
})}
{categories.map((category) => (
<Area
{...areaProps}
className={
getColorClassNames(
categoryColors.get(category) ?? BaseColors.Gray,
colorPalette.text
).strokeColor
}
strokeOpacity={
activeDot || (activeLegend && activeLegend !== category)
? 0.3
: 1
}
// eslint-disable-next-line @typescript-eslint/no-shadow
activeDot={(props: any) => {
const {
cx,
cy,
stroke,
strokeLinecap,
strokeLinejoin,
strokeWidth,
dataKey,
} = props;
return (
<Dot
className={tremorTwMerge(
'stroke-tremor-background dark:stroke-dark-tremor-background',
onValueChange ? 'cursor-pointer' : '',
getColorClassNames(
categoryColors.get(dataKey) ?? BaseColors.Gray,
colorPalette.text
).fillColor
)}
cx={cx}
cy={cy}
r={5}
fill=""
stroke={stroke}
strokeLinecap={strokeLinecap}
strokeLinejoin={strokeLinejoin}
strokeWidth={strokeWidth}
onClick={(dotProps: any, event) =>
onDotClick(props, event)
}
/>
);
}}
// eslint-disable-next-line @typescript-eslint/no-shadow
dot={(props: any) => {
const {
stroke,
strokeLinecap,
strokeLinejoin,
strokeWidth,
cx,
cy,
dataKey,
// eslint-disable-next-line @typescript-eslint/no-shadow
index,
} = props;
if (
(hasOnlyOneValueForThisKey(data, category) &&
!(
activeDot ||
(activeLegend && activeLegend !== category)
)) ||
(activeDot?.index === index &&
activeDot?.dataKey === category)
) {
return (
<Dot
key={index}
cx={cx}
cy={cy}
r={5}
stroke={stroke}
fill=""
strokeLinecap={strokeLinecap}
strokeLinejoin={strokeLinejoin}
strokeWidth={strokeWidth}
className={tremorTwMerge(
'stroke-tremor-background dark:stroke-dark-tremor-background',
onValueChange ? 'cursor-pointer' : '',
getColorClassNames(
categoryColors.get(dataKey) ?? BaseColors.Gray,
colorPalette.text
).fillColor
)}
/>
);
}
return <Fragment key={index}></Fragment>;
}}
key={category}
name={category}
type={curveType}
dataKey={category}
stroke=""
fill={`url(#${categoryColors.get(category)})`}
strokeWidth={2}
strokeLinejoin="round"
strokeLinecap="round"
isAnimationActive={showAnimation}
animationDuration={animationDuration}
stackId={stack ? 'a' : undefined}
connectNulls={connectNulls}
/>
))}
{onValueChange
? categories.map((category) => (
<Line
className={tremorTwMerge('cursor-pointer')}
strokeOpacity={0}
key={category}
name={category}
type={curveType}
dataKey={category}
stroke="transparent"
fill="transparent"
legendType="none"
tooltipType="none"
strokeWidth={12}
connectNulls={connectNulls}
// eslint-disable-next-line @typescript-eslint/no-shadow
onClick={(props: any, event) => {
event.stopPropagation();
const { name } = props;
onCategoryClick(name);
}}
/>
))
: null}
</ReChartsAreaChart>
) : (
<NoData noDataText={noDataText} />
)}
</ResponsiveContainer>
</div>
);
}
);
AreaChart.displayName = 'AreaChart';
export default AreaChart;
Thank you for reporting back!
What problem does this feature solve?
I want to reverse the y-axis of an area chart so that it goes from low to high instead of high to low. I know this can be done with Recharts, but I'm not sure how to achieve it with Tremor. Does anyone know any solutions for this?
What does the proposed API look like?
No response