diff --git a/src/chart/generateCategoricalChart.tsx b/src/chart/generateCategoricalChart.tsx index d97d8bdd67..5499f9f2bc 100644 --- a/src/chart/generateCategoricalChart.tsx +++ b/src/chart/generateCategoricalChart.tsx @@ -78,6 +78,7 @@ import { } from '../util/types'; import { AccessibilityManager } from './AccessibilityManager'; import { isDomainSpecifiedByUser } from '../util/isDomainSpecifiedByUser'; +import { StepRatioControl } from '../util/scale/getNiceTickValues'; import { ChartLayoutContextProvider } from '../context/chartLayoutContext'; import { AxisMap, CategoricalChartState } from './types'; import { AccessibilityContextProvider } from '../context/accessibilityContext'; @@ -802,6 +803,7 @@ export interface CategoricalChartProps { data?: any[]; layout?: LayoutType; stackOffset?: StackOffsetType; + stepRatioControl?: StepRatioControl; throttleDelay?: number; margin?: Margin; barCategoryGap?: number | string; @@ -1006,7 +1008,7 @@ export const generateCategoricalChart = ({ return null; } - const { children, layout, stackOffset, data, reverseStackOrder } = props; + const { children, layout, stackOffset, data, reverseStackOrder, stepRatioControl } = props; const { numericAxisName, cateAxisName } = getAxisNameByLayout(layout); const graphicalItems = findAllByType(children, GraphicalChild); const stackGroups: AxisStackGroups = getStackGroupsByAxisId( @@ -1035,7 +1037,7 @@ export const generateCategoricalChart = ({ const offset: ChartOffset = calculateOffset({ ...axisObj, props }, prevState?.legendBBox); Object.keys(axisObj).forEach(key => { - axisObj[key] = formatAxisMap(props, axisObj[key], offset, key.replace('Map', ''), chartName); + axisObj[key] = formatAxisMap(props, axisObj[key], offset, key.replace('Map', ''), chartName, stepRatioControl); }); const cateAxisMap = axisObj[`${cateAxisName}Map`]; const ticksObj = tooltipTicksGenerator(cateAxisMap); @@ -1080,6 +1082,7 @@ export const generateCategoricalChart = ({ margin: { top: 5, right: 5, bottom: 5, left: 5 } as Margin, reverseStackOrder: false, syncMethod: 'index', + stepRatioControl: 0.05, ...defaultProps, }; @@ -1219,7 +1222,7 @@ export const generateCategoricalChart = ({ nextProps: CategoricalChartProps, prevState: CategoricalChartState, ): CategoricalChartState => { - const { dataKey, data, children, width, height, layout, stackOffset, margin } = nextProps; + const { dataKey, data, children, width, height, layout, stackOffset, margin, stepRatioControl } = nextProps; const { dataStartIndex, dataEndIndex } = prevState; if (prevState.updateId === undefined) { @@ -1242,6 +1245,7 @@ export const generateCategoricalChart = ({ prevHeight: height, prevLayout: layout, prevStackOffset: stackOffset, + prevStepRatioControl: stepRatioControl, prevMargin: margin, prevChildren: children, }; @@ -1253,6 +1257,7 @@ export const generateCategoricalChart = ({ height !== prevState.prevHeight || layout !== prevState.prevLayout || stackOffset !== prevState.prevStackOffset || + stepRatioControl !== prevState.prevStepRatioControl || !shallowEqual(margin, prevState.prevMargin) ) { const defaultState = createDefaultState(nextProps); @@ -1296,6 +1301,7 @@ export const generateCategoricalChart = ({ prevHeight: height, prevLayout: layout, prevStackOffset: stackOffset, + prevStepRatioControl: stepRatioControl, prevMargin: margin, prevChildren: children, }; @@ -1856,7 +1862,8 @@ export const generateCategoricalChart = ({ return null; } - const { children, className, width, height, style, compact, title, desc, ...others } = this.props; + const { children, className, width, height, style, compact, title, desc, stepRatioControl, ...others } = + this.props; const attrs = filterProps(others, false); // The "compact" mode is mainly used as the panorama within Brush diff --git a/src/chart/types.ts b/src/chart/types.ts index 1c54e65d20..1494c623dd 100644 --- a/src/chart/types.ts +++ b/src/chart/types.ts @@ -11,6 +11,7 @@ import { } from '../util/types'; import { AxisStackGroups } from '../util/ChartUtils'; import { BoundingBox } from '../util/useGetBoundingClientRect'; +import { StepRatioControl } from '../util/scale/getNiceTickValues'; export type AxisMap = { [axisId: string]: BaseAxisProps; @@ -82,6 +83,7 @@ export interface CategoricalChartState { prevStackOffset?: StackOffsetType; prevMargin?: Margin; prevChildren?: any; + prevStepRatioControl?: StepRatioControl; stackGroups?: AxisStackGroups; tooltipPortal?: HTMLElement | null; diff --git a/src/util/CartesianUtils.ts b/src/util/CartesianUtils.ts index 1d62b48598..d87b732b7d 100644 --- a/src/util/CartesianUtils.ts +++ b/src/util/CartesianUtils.ts @@ -6,6 +6,7 @@ import { findChildByType } from './ReactUtils'; import { Coordinate, AxisType, Size } from './types'; import { getPercentValue } from './DataUtils'; import { Bar } from '../cartesian/Bar'; +import { StepRatioControl } from './scale/getNiceTickValues'; /** * Calculate the scale function, position, width, height of axes @@ -14,9 +15,17 @@ import { Bar } from '../cartesian/Bar'; * @param {Object} offset The offset of main part in the svg element * @param {String} axisType The type of axes, x-axis or y-axis * @param {String} chartName The name of chart + * @param {StepRatioControl} stepRatioControl The value to control the step of y domain * @return {Object} Configuration */ -export const formatAxisMap = (props: any, axisMap: any, offset: any, axisType: AxisType, chartName: string) => { +export const formatAxisMap = ( + props: any, + axisMap: any, + offset: any, + axisType: AxisType, + chartName: string, + stepRatioControl: StepRatioControl = 0.05, +) => { const { width, height, layout, children } = props; const ids = Object.keys(axisMap); const steps: Record = { @@ -90,7 +99,7 @@ export const formatAxisMap = (props: any, axisMap: any, offset: any, axisType: A const { scale, realScaleType } = parseScale(axis, chartName, hasBar); scale.domain(domain).range(range); checkDomainOfScale(scale); - const ticks = getTicksOfScale(scale, { ...axis, realScaleType }); + const ticks = getTicksOfScale(scale, { ...axis, realScaleType, stepRatioControl }); if (axisType === 'xAxis') { needSpace = (orientation === 'top' && !mirror) || (orientation === 'bottom' && mirror); diff --git a/src/util/ChartUtils.ts b/src/util/ChartUtils.ts index f2884ab5ef..4cf24dfd86 100644 --- a/src/util/ChartUtils.ts +++ b/src/util/ChartUtils.ts @@ -1051,7 +1051,7 @@ export const getStackGroupsByAxisId = ( * @return {Object} null */ export const getTicksOfScale = (scale: any, opts: any) => { - const { realScaleType, type, tickCount, originalDomain, allowDecimals } = opts; + const { realScaleType, type, tickCount, originalDomain, allowDecimals, stepRatioControl } = opts; const scaleType = realScaleType || opts.scale; if (scaleType !== 'auto' && scaleType !== 'linear') { @@ -1070,7 +1070,7 @@ export const getTicksOfScale = (scale: any, opts: any) => { return null; } - const tickValues = getNiceTickValues(domain, tickCount, allowDecimals); + const tickValues = getNiceTickValues(domain, tickCount, allowDecimals, stepRatioControl); scale.domain([min(tickValues), max(tickValues)]); return { niceTicks: tickValues }; diff --git a/src/util/scale/getNiceTickValues.ts b/src/util/scale/getNiceTickValues.ts index fca4289615..075dc68324 100644 --- a/src/util/scale/getNiceTickValues.ts +++ b/src/util/scale/getNiceTickValues.ts @@ -7,6 +7,8 @@ import Decimal from 'decimal.js-light'; import { compose, range, memoize, map, reverse } from './util/utils'; import { getDigitCount, rangeStep } from './util/arithmetic'; +export type StepRatioControl = 0.05 | 0.03 | 0.01; + /** * Calculate a interval of a minimum value and a maximum value * @@ -32,9 +34,15 @@ export const getValidInterval = ([min, max]: [number, number]) => { * difference by the tickCount * @param {Boolean} allowDecimals Allow the ticks to be decimals or not * @param {Integer} correctionFactor A correction factor + * @param {StepRatioControl} stepRatioControl The value to control the step of y domain * @return {Decimal} The step which is easy to understand between two ticks */ -export const getFormatStep = (roughStep: Decimal, allowDecimals: boolean, correctionFactor: number) => { +export const getFormatStep = ( + roughStep: Decimal, + allowDecimals: boolean, + correctionFactor: number, + stepRatioControl: StepRatioControl = 0.05, +) => { if (roughStep.lte(0)) { return new Decimal(0); } @@ -45,7 +53,7 @@ export const getFormatStep = (roughStep: Decimal, allowDecimals: boolean, correc const digitCountValue = new Decimal(10).pow(digitCount); const stepRatio = roughStep.div(digitCountValue); // When an integer and a float multiplied, the accuracy of result may be wrong - const stepRatioScale = digitCount !== 1 ? 0.05 : 0.1; + const stepRatioScale = digitCount !== 1 ? stepRatioControl : 0.1; const amendStepRatio = new Decimal(Math.ceil(stepRatio.div(stepRatioScale).toNumber())) .add(correctionFactor) .mul(stepRatioScale); @@ -104,6 +112,7 @@ export const getTickOfSingleValue = (value: number, tickCount: number, allowDeci * @param {Integer} tickCount The count of ticks * @param {Boolean} allowDecimals Allow the ticks to be decimals or not * @param {Number} correctionFactor A correction factor + * @param {StepRatioControl} stepRatioControl The value to control the step of y domain * @return {Object} The step, minimum value of ticks, maximum value of ticks */ export const calculateStep = ( @@ -112,6 +121,7 @@ export const calculateStep = ( tickCount: number, allowDecimals: boolean, correctionFactor = 0, + stepRatioControl: StepRatioControl = 0.05, ): any => { // dirty hack (for recharts' test) if (!Number.isFinite((max - min) / (tickCount - 1))) { @@ -123,7 +133,12 @@ export const calculateStep = ( } // The step which is easy to understand between two ticks - const step = getFormatStep(new Decimal(max).sub(min).div(tickCount - 1), allowDecimals, correctionFactor); + const step = getFormatStep( + new Decimal(max).sub(min).div(tickCount - 1), + allowDecimals, + correctionFactor, + stepRatioControl, + ); // A medial value of ticks let middle; @@ -144,7 +159,7 @@ export const calculateStep = ( if (scaleCount > tickCount) { // When more ticks need to cover the interval, step should be bigger. - return calculateStep(min, max, tickCount, allowDecimals, correctionFactor + 1); + return calculateStep(min, max, tickCount, allowDecimals, correctionFactor + 1, stepRatioControl); } if (scaleCount < tickCount) { // When less ticks can cover the interval, we should add some additional ticks @@ -165,9 +180,15 @@ export const calculateStep = ( * @param {Number} min, max min: The minimum value, max: The maximum value * @param {Integer} tickCount The count of ticks * @param {Boolean} allowDecimals Allow the ticks to be decimals or not + * @param {StepRatioControl} stepRatioControl The value to control the step of y domain * @return {Array} ticks */ -function getNiceTickValuesFn([min, max]: [number, number], tickCount = 6, allowDecimals = true) { +function getNiceTickValuesFn( + [min, max]: [number, number], + tickCount = 6, + allowDecimals = true, + stepRatioControl: StepRatioControl = 0.05, +) { // More than two ticks should be return const count = Math.max(tickCount, 2); const [cormin, cormax] = getValidInterval([min, max]); @@ -186,7 +207,7 @@ function getNiceTickValuesFn([min, max]: [number, number], tickCount = 6, allowD } // Get the step between two ticks - const { step, tickMin, tickMax } = calculateStep(cormin, cormax, count, allowDecimals, 0); + const { step, tickMin, tickMax } = calculateStep(cormin, cormax, count, allowDecimals, 0, stepRatioControl); const values = rangeStep(tickMin, tickMax.add(new Decimal(0.1).mul(step)), step); diff --git a/storybook/stories/API/chart/BarChart.stories.tsx b/storybook/stories/API/chart/BarChart.stories.tsx index f088368f8b..6f390ee1d2 100644 --- a/storybook/stories/API/chart/BarChart.stories.tsx +++ b/storybook/stories/API/chart/BarChart.stories.tsx @@ -64,6 +64,7 @@ export const Stacked = { args: { data: pageDataWithNegativeNumbers, stackOffset: 'none', + stepRatioControl: 0.05, id: 'BarChart-Stacked', }, }; diff --git a/storybook/stories/API/props/ChartProps.ts b/storybook/stories/API/props/ChartProps.ts index 09d27cbf3a..88f241dcc1 100644 --- a/storybook/stories/API/props/ChartProps.ts +++ b/storybook/stories/API/props/ChartProps.ts @@ -181,6 +181,24 @@ toggling between multiple dataKey.`, category: 'General', }, }, + stepRatioControl: { + description: `This parameter controls the domain range for the y-axis and the step increments within that domain. + A lower value for this parameter moves the maximum y-axis value closer to the highest data point in the dataset. + For example, with the dataset [0, 400, 800, 1200, 1600], a stepRatioControl value of 0.05 would set the y-axis domain to [0, 2000]. + Conversely, setting stepRatioControl to 0.1 brings the y-axis domain closer to [0, 1600]. + Regardless of the parameter chosen, the step sizes within the domain remain even and will have "nice" values. + Adjusting this parameter is recommended only when a minor change to the dataset's maximum/minimum values causes the default domain to shift dramatically.`, + options: ['0.01', '0.03', '0.05'], + control: { + type: 'select', + }, + table: { + type: { + summary: 'number', + }, + category: 'General', + }, + }, cx: { description: 'The x-coordinate of the center of the circle.', table: { diff --git a/test/cartesian/YAxis.spec.tsx b/test/cartesian/YAxis.spec.tsx index 2380137edc..6755f9b91a 100644 --- a/test/cartesian/YAxis.spec.tsx +++ b/test/cartesian/YAxis.spec.tsx @@ -295,4 +295,70 @@ describe('', () => { expect(allText).toContain('1200'); expect(allText).toContain('1600'); }); + + it('should render all labels when stepRatioControl is 0.03', () => { + const { container } = render( + + + + + + + , + ); + const allLabels = container.querySelectorAll('.recharts-yAxis .recharts-text.recharts-cartesian-axis-tick-value'); + expect.soft(allLabels).toHaveLength(5); + const allText = Array.from(allLabels).map(el => el.textContent); + expect.soft(allText).toHaveLength(5); + expect(allText).toContain('0'); + expect(allText).toContain('390'); + expect(allText).toContain('780'); + expect(allText).toContain('1170'); + expect(allText).toContain('1560'); + }); + + it('should render all labels when stepRatioControl is 0.01', () => { + const { container } = render( + + + + + + + , + ); + const allLabels = container.querySelectorAll('.recharts-yAxis .recharts-text.recharts-cartesian-axis-tick-value'); + expect.soft(allLabels).toHaveLength(5); + const allText = Array.from(allLabels).map(el => el.textContent); + expect.soft(allText).toHaveLength(5); + expect(allText).toContain('0'); + expect(allText).toContain('380'); + expect(allText).toContain('760'); + expect(allText).toContain('1140'); + expect(allText).toContain('1520'); + }); }); diff --git a/test/util/ChartUtils.spec.tsx b/test/util/ChartUtils.spec.tsx index ec132ae497..25cf8547f4 100644 --- a/test/util/ChartUtils.spec.tsx +++ b/test/util/ChartUtils.spec.tsx @@ -349,6 +349,22 @@ describe('getTicksOfScale', () => { expect(result?.niceTicks).toEqual([0, 0.25, 0.5, 0.75, 1]); }); + + it('should generate correct tick values with stepRatioControl set to 0.03', () => { + const scale = scaleLinear(); + const opts = { + scale: 'linear', + type: 'number', + tickCount: 5, + originalDomain: ['auto', 'auto'], + allowDecimals: true, + stepRatioControl: 0.03, + }; + + const result = getTicksOfScale(scale, opts); + + expect(result?.niceTicks).toEqual([0, 0.27, 0.54, 0.81, 1.08]); + }); }); describe('calculateActiveTickIndex', () => {