Skip to content

Commit

Permalink
Add stepRatioControl
Browse files Browse the repository at this point in the history
  • Loading branch information
ForestLinSen committed Apr 24, 2024
1 parent ddba2c5 commit 90f4f15
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 14 deletions.
15 changes: 11 additions & 4 deletions src/chart/generateCategoricalChart.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -802,6 +803,7 @@ export interface CategoricalChartProps {
data?: any[];
layout?: LayoutType;
stackOffset?: StackOffsetType;
stepRatioControl?: StepRatioControl;
throttleDelay?: number;
margin?: Margin;
barCategoryGap?: number | string;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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) {
Expand All @@ -1242,6 +1245,7 @@ export const generateCategoricalChart = ({
prevHeight: height,
prevLayout: layout,
prevStackOffset: stackOffset,
prevStepRatioControl: stepRatioControl,
prevMargin: margin,
prevChildren: children,
};
Expand All @@ -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);
Expand Down Expand Up @@ -1296,6 +1301,7 @@ export const generateCategoricalChart = ({
prevHeight: height,
prevLayout: layout,
prevStackOffset: stackOffset,
prevStepRatioControl: stepRatioControl,
prevMargin: margin,
prevChildren: children,
};
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/chart/types.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -82,6 +83,7 @@ export interface CategoricalChartState {
prevStackOffset?: StackOffsetType;
prevMargin?: Margin;
prevChildren?: any;
prevStepRatioControl?: StepRatioControl;
stackGroups?: AxisStackGroups;

tooltipPortal?: HTMLElement | null;
Expand Down
13 changes: 11 additions & 2 deletions src/util/CartesianUtils.ts
Expand Up @@ -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
Expand All @@ -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<string, any> = {
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/util/ChartUtils.ts
Expand Up @@ -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') {
Expand All @@ -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 };
Expand Down
33 changes: 27 additions & 6 deletions src/util/scale/getNiceTickValues.ts
Expand Up @@ -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
*
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down Expand Up @@ -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 = (
Expand All @@ -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))) {
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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]);
Expand All @@ -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);

Expand Down
1 change: 1 addition & 0 deletions storybook/stories/API/chart/BarChart.stories.tsx
Expand Up @@ -64,6 +64,7 @@ export const Stacked = {
args: {
data: pageDataWithNegativeNumbers,
stackOffset: 'none',
stepRatioControl: 0.05,
id: 'BarChart-Stacked',
},
};
18 changes: 18 additions & 0 deletions storybook/stories/API/props/ChartProps.ts
Expand Up @@ -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: {
Expand Down
66 changes: 66 additions & 0 deletions test/cartesian/YAxis.spec.tsx
Expand Up @@ -295,4 +295,70 @@ describe('<YAxis />', () => {
expect(allText).toContain('1200');
expect(allText).toContain('1600');
});

it('should render all labels when stepRatioControl is 0.03', () => {
const { container } = render(
<LineChart
width={500}
height={300}
data={pageData}
accessibilityLayer
stepRatioControl={0.03}
margin={{
top: 5,
right: 5,
bottom: 5,
left: 0,
}}
>
<YAxis />
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
<Line type="monotone" dataKey="uv" stroke="#8884d8" />
<Line type="monotone" dataKey="pv" stroke="#82ca9d" />
<Tooltip />
</LineChart>,
);
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(
<LineChart
width={500}
height={300}
data={pageData}
accessibilityLayer
stepRatioControl={0.01}
margin={{
top: 5,
right: 5,
bottom: 5,
left: 0,
}}
>
<YAxis />
<CartesianGrid stroke="#eee" strokeDasharray="5 5" />
<Line type="monotone" dataKey="uv" stroke="#8884d8" />
<Line type="monotone" dataKey="pv" stroke="#82ca9d" />
<Tooltip />
</LineChart>,
);
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');
});
});
16 changes: 16 additions & 0 deletions test/util/ChartUtils.spec.tsx
Expand Up @@ -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', () => {
Expand Down

0 comments on commit 90f4f15

Please sign in to comment.