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

Automatically adjust stepRatioScale to fix weired behaviour when calculating yAxis-domain #4453

Open
wants to merge 4 commits into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/chart/generateCategoricalChart.tsx
Original file line number Diff line number Diff line change
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one idea I had was, instead of making this a separate parameter that gets passed to every chart at the highest level why don't we associate it with the domain (since thats where it is used). For example, we can keep everything as-is but allow an object as part of domain specification.

domain={'auto'}

OR

domain={[0, 10000]}

OR

`domain={(min, max) => [min, max]}

OR

domain={{ domain: 'auto' , settings: stepRatioControl: 0.1 }}
(or maybe somehow only allow it if domain is auto so there is no confusion)

Or if not the above then

domainSettings={{ stepRatioControl: 0.1 }}

on the X/YAxis?

This way there is less ambiguity against what the parameter affects/changes. Otherwise it seems arbitrary and hard to follow like the discussion in the other thread...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think combining this parameter with something is a good idea, but my concern is that it would require a lot of changes to the code that currently assumes the input value is an array containing two numeric values when the domain is set to auto - changing it to an object would add a lot messy. Another concern is that making it an object allows users to set any number, which might be problematic if the value is being set to too high/ low, without the enum type they would need an even better understanding of how to change the value. Anyway I like the idea, just wondering if there's a better way to combine this parameter

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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