Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature]: Reversed Y-Axis #1022

Open
Dib5pm opened this issue Apr 25, 2024 · 2 comments
Open

[Feature]: Reversed Y-Axis #1022

Dib5pm opened this issue Apr 25, 2024 · 2 comments
Labels
Type: Feature New feature for existing component

Comments

@Dib5pm
Copy link

Dib5pm commented Apr 25, 2024

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

@Dib5pm
Copy link
Author

Dib5pm commented Apr 26, 2024

Solution Reversed Y-Axis

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.

Issue

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,
         }}

Example Implementation

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>
    );
  }
}

Full Implementation for the Tremor AreaChart Component

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;

@Dib5pm Dib5pm changed the title [Feature]: Inverse Yaxis [Feature]: Reversed Y-Axis Apr 26, 2024
@severinlandolt
Copy link
Member

Thank you for reporting back!

@severinlandolt severinlandolt added the Type: Feature New feature for existing component label Apr 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Feature New feature for existing component
Projects
None yet
Development

No branches or pull requests

2 participants